diff options
Diffstat (limited to 'addons/batikvis/src/main')
215 files changed, 36387 insertions, 0 deletions
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/application/greedyensemble/VisualizePairwiseGainMatrix.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/application/greedyensemble/VisualizePairwiseGainMatrix.java new file mode 100644 index 00000000..28435915 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/application/greedyensemble/VisualizePairwiseGainMatrix.java @@ -0,0 +1,350 @@ +package de.lmu.ifi.dbs.elki.application.greedyensemble; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.BufferedImage; + +import org.apache.batik.util.SVGConstants; + +import de.lmu.ifi.dbs.elki.application.AbstractApplication; +import de.lmu.ifi.dbs.elki.data.NumberVector; +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.ArrayModifiableDBIDs; +import de.lmu.ifi.dbs.elki.database.ids.DBID; +import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.evaluation.scores.ROCEvaluation; +import de.lmu.ifi.dbs.elki.evaluation.scores.adapter.DecreasingVectorIter; +import de.lmu.ifi.dbs.elki.evaluation.scores.adapter.VectorNonZero; +import de.lmu.ifi.dbs.elki.evaluation.similaritymatrix.ComputeSimilarityMatrixImage; +import de.lmu.ifi.dbs.elki.evaluation.similaritymatrix.ComputeSimilarityMatrixImage.SimilarityMatrix; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.logging.progress.FiniteProgress; +import de.lmu.ifi.dbs.elki.math.DoubleMinMax; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.result.ResultHierarchy; +import de.lmu.ifi.dbs.elki.utilities.DatabaseUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.documentation.Reference; +import de.lmu.ifi.dbs.elki.utilities.ensemble.EnsembleVoting; +import de.lmu.ifi.dbs.elki.utilities.ensemble.EnsembleVotingMean; +import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ObjectParameter; +import de.lmu.ifi.dbs.elki.utilities.scaling.LinearScaling; +import de.lmu.ifi.dbs.elki.utilities.scaling.ScalingFunction; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.VisualizerParameterizer; +import de.lmu.ifi.dbs.elki.visualization.gui.SimpleSVGViewer; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SimilarityMatrixVisualizer; +import de.lmu.ifi.dbs.elki.workflow.InputStep; + +/** + * Class to load an outlier detection summary file, as produced by + * {@link ComputeKNNOutlierScores}, and compute a matrix with the pairwise + * gains. It will have one column / row obtained for each combination. + * + * The gain is always computed in relation to the better of the two input + * methods. Green colors indicate the result has improved, red indicate it + * became worse. + * + * Reference: + * <p> + * E. Schubert, R. Wojdanowski, A. Zimek, H.-P. Kriegel<br /> + * On Evaluation of Outlier Rankings and Outlier Scores<br/> + * In Proceedings of the 12th SIAM International Conference on Data Mining + * (SDM), Anaheim, CA, 2012. + * </p> + * + * @author Erich Schubert + * + * @apiviz.composedOf VisualizerParameterizer + * @apiviz.composedOf SimilarityMatrixVisualizer + */ +@Reference(authors = "E. Schubert, R. Wojdanowski, A. Zimek, H.-P. Kriegel", // +title = "On Evaluation of Outlier Rankings and Outlier Scores", // +booktitle = "Proc. 12th SIAM International Conference on Data Mining (SDM), Anaheim, CA, 2012.", // +url = "http://dx.doi.org/10.1137/1.9781611972825.90") +public class VisualizePairwiseGainMatrix extends AbstractApplication { + /** + * Get static logger. + */ + private static final Logging LOG = Logging.getLogger(VisualizePairwiseGainMatrix.class); + + /** + * The data input part. + */ + private InputStep inputstep; + + /** + * Parameterizer for visualizers. + */ + private VisualizerParameterizer vispar; + + /** + * Outlier scaling to apply during preprocessing. + */ + private ScalingFunction prescaling; + + /** + * Ensemble voting function. + */ + private EnsembleVoting voting; + + /** + * Constructor. + * + * @param inputstep Input step + * @param prescaling Scaling function for input scores. + * @param voting Voting function + * @param vispar Visualizer parameterizer + */ + public VisualizePairwiseGainMatrix(InputStep inputstep, ScalingFunction prescaling, EnsembleVoting voting, VisualizerParameterizer vispar) { + super(); + this.inputstep = inputstep; + this.prescaling = prescaling; + this.voting = voting; + this.vispar = vispar; + } + + @Override + public void run() { + final Database database = inputstep.getDatabase(); + ResultHierarchy hier = database.getHierarchy(); + Relation<NumberVector> relation = database.getRelation(TypeUtil.NUMBER_VECTOR_FIELD); + final Relation<String> labels = DatabaseUtil.guessLabelRepresentation(database); + final DBID firstid = DBIDUtil.deref(labels.iterDBIDs()); + final String firstlabel = labels.get(firstid); + if(!firstlabel.matches(".*by.?label.*")) { + throw new AbortException("No 'by label' reference outlier found, which is needed for weighting!"); + } + relation = GreedyEnsembleExperiment.applyPrescaling(prescaling, relation, firstid); + + // Dimensionality and reference vector + final int dim = RelationUtil.dimensionality(relation); + final NumberVector refvec = relation.get(firstid); + + // Build the truth vector + VectorNonZero pos = new VectorNonZero(refvec); + + ArrayModifiableDBIDs ids = DBIDUtil.newArray(relation.getDBIDs()); + ids.remove(firstid); + ids.sort(); + final int size = ids.size(); + + double[][] data = new double[size][size]; + DoubleMinMax minmax = new DoubleMinMax(), commax = new DoubleMinMax(); + + { + FiniteProgress prog = LOG.isVerbose() ? new FiniteProgress("Computing ensemble gain.", size * (size + 1) >> 1, LOG) : null; + double[] buf = new double[2]; // Vote combination buffer. + int a = 0; + for(DBIDIter id = ids.iter(); id.valid(); id.advance(), a++) { + final NumberVector veca = relation.get(id); + // Direct AUC score: + { + double auc = ROCEvaluation.computeROCAUC(pos, new DecreasingVectorIter(veca)); + data[a][a] = auc; + // minmax.put(auc); + LOG.incrementProcessed(prog); + } + // Compare to others, exploiting symmetry + DBIDArrayIter id2 = ids.iter(); + id2.seek(a + 1); + for(int b = a + 1; b < size; b++, id2.advance()) { + final NumberVector vecb = relation.get(id2); + double[] combined = new double[dim]; + for(int d = 0; d < dim; d++) { + buf[0] = veca.doubleValue(d); + buf[1] = vecb.doubleValue(d); + combined[d] = voting.combine(buf); + } + double auc = ROCEvaluation.computeROCAUC(pos, new DecreasingVectorIter(new Vector(combined))); + // logger.verbose(auc + " " + labels.get(ids.get(a)) + " " + + // labels.get(ids.get(b))); + data[a][b] = auc; + data[b][a] = auc; + commax.put(data[a][b]); + // minmax.put(auc); + LOG.incrementProcessed(prog); + } + } + LOG.ensureCompleted(prog); + } + for(int a = 0; a < size; a++) { + for(int b = a + 1; b < size; b++) { + double ref = Math.max(data[a][a], data[b][b]); + data[a][b] = (data[a][b] - ref) / (1 - ref); + data[b][a] = (data[b][a] - ref) / (1 - ref); + // logger.verbose(data[a][b] + " " + labels.get(ids.get(a)) + " " + + // labels.get(ids.get(b))); + minmax.put(data[a][b]); + } + } + for(int a = 0; a < size; a++) { + data[a][a] = 0; + } + + LOG.verbose("Gain: " + minmax.toString() + " AUC: " + commax.toString()); + + boolean hasneg = (minmax.getMin() < -1E-3); + LinearScaling scale; + if(!hasneg) { + scale = LinearScaling.fromMinMax(0., minmax.getMax()); + } + else { + scale = LinearScaling.fromMinMax(0.0, Math.max(minmax.getMax(), -minmax.getMin())); + } + scale = LinearScaling.fromMinMax(0., .5); + + BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); + for(int x = 0; x < size; x++) { + for(int y = x; y < size; y++) { + double val = data[x][y]; + val = Math.max(-1, Math.min(1., scale.getScaled(val))); + // Compute color: + final int col; + { + if(val >= 0) { + int ival = 0xFF & (int) (255 * val); + col = 0xff000000 | (ival << 8); + } + else { + int ival = 0xFF & (int) (255 * -val); + col = 0xff000000 | (ival << 16); + } + } + img.setRGB(x, y, col); + img.setRGB(y, x, col); + } + } + SimilarityMatrix smat = new ComputeSimilarityMatrixImage.SimilarityMatrix(img, relation, ids); + hier.add(database, smat); + + VisualizerContext context = vispar.newContext(hier, smat); + + // Attach visualizers to results + SimilarityMatrixVisualizer factory = new SimilarityMatrixVisualizer(); + factory.processNewResult(context, database); + + Hierarchy.Iter<VisualizationTask> it = VisualizationTree.filter(context, VisualizationTask.class); + for(; it.valid(); it.advance()) { + VisualizationTask task = it.get(); + if(task.getFactory() == factory) { + showVisualization(context, factory, task); + } + } + } + + /** + * Show a single visualization. + * + * @param context Visualization context + * @param factory Visualizer factory + * @param task Visualization task + */ + private void showVisualization(VisualizerContext context, SimilarityMatrixVisualizer factory, VisualizationTask task) { + VisualizationPlot plot = new VisualizationPlot(); + Visualization vis = factory.makeVisualization(task, plot, 1.0, 1.0, null); + plot.getRoot().appendChild(vis.getLayer()); + plot.getRoot().setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm"); + plot.getRoot().setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "20cm"); + plot.getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 1 1"); + plot.updateStyleElement(); + + (new SimpleSVGViewer()).setPlot(plot); + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractApplication.Parameterizer { + /** + * Data source. + */ + private InputStep inputstep; + + /** + * Parameterizer for visualizers. + */ + private VisualizerParameterizer vispar; + + /** + * Outlier scaling to apply during preprocessing. + */ + private ScalingFunction prescaling; + + /** + * Voring function. + */ + private EnsembleVoting voting; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + // Data input + inputstep = config.tryInstantiate(InputStep.class); + // Visualization options + vispar = config.tryInstantiate(VisualizerParameterizer.class); + + // Prescaling + ObjectParameter<ScalingFunction> prescalingP = new ObjectParameter<>(GreedyEnsembleExperiment.Parameterizer.PRESCALING_ID, ScalingFunction.class); + prescalingP.setOptional(true); + if(config.grab(prescalingP)) { + prescaling = prescalingP.instantiateClass(config); + } + + ObjectParameter<EnsembleVoting> votingP = new ObjectParameter<>(GreedyEnsembleExperiment.Parameterizer.VOTING_ID, EnsembleVoting.class, EnsembleVotingMean.class); + if(config.grab(votingP)) { + voting = votingP.instantiateClass(config); + } + } + + @Override + protected VisualizePairwiseGainMatrix makeInstance() { + return new VisualizePairwiseGainMatrix(inputstep, prescaling, voting, vispar); + } + } + + /** + * Main method. + * + * @param args Command line parameters. + */ + public static void main(String[] args) { + runCLIApplication(VisualizePairwiseGainMatrix.class, args); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/ExportVisualizations.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/ExportVisualizations.java new file mode 100644 index 00000000..bf552880 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/ExportVisualizations.java @@ -0,0 +1,296 @@ +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 <http://www.gnu.org/licenses/>. + */ +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.batik.util.SVGConstants; + +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultHandler; +import de.lmu.ifi.dbs.elki.result.ResultHierarchy; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +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.constraints.CommonConstraints; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.DoubleParameter; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.FileParameter; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.FileParameter.FileType; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Class that automatically generates all visualizations and exports them into + * SVG files. To configure the export, you <em>will</em> want to configure the + * {@link VisualizerParameterizer}, in particular the pattern for choosing which + * visualizers to run. + * + * @author Erich Schubert + * + * @apiviz.composedOf VisualizerParameterizer + */ +public class ExportVisualizations implements ResultHandler { + /** + * Get a logger for this class. + */ + private static final Logging LOG = Logging.getLogger(ExportVisualizations.class); + + /** + * Output folder + */ + File output; + + /** + * Visualization manager. + */ + VisualizerParameterizer manager; + + /** + * Ratio for canvas + */ + double ratio; + + /** + * Base result + */ + Result baseResult = null; + + /** + * Visualizer context + */ + VisualizerContext context = null; + + /** + * Output counter. + */ + Map<String, Integer> counter = new HashMap<>(); + + /** + * Constructor. + * + * @param output Output folder + * @param manager Parameterizer + * @param ratio Canvas ratio + */ + public ExportVisualizations(File output, VisualizerParameterizer manager, double ratio) { + super(); + this.output = output; + this.manager = manager; + this.ratio = ratio; + } + + @Override + public void processNewResult(ResultHierarchy hier, Result newResult) { + if(output.isFile()) { + throw new AbortException("Output folder cannot be an existing file."); + } + if(!output.exists()) { + if(!output.mkdirs()) { + throw new AbortException("Could not create output directory."); + } + } + if(this.baseResult == null) { + this.baseResult = newResult; + context = null; + counter = new HashMap<>(); + LOG.warning("Note: Reusing visualization exporter for more than one result is untested."); + } + if(context == null) { + context = manager.newContext(hier, baseResult); + } + + // Projected visualizations + Hierarchy<Object> vistree = context.getVisHierarchy(); + for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) { + if(!(iter2.get() instanceof Projector)) { + continue; + } + Projector proj = (Projector) iter2.get(); + // TODO: allow selecting individual projections only. + Collection<PlotItem> items = proj.arrange(context); + for(PlotItem item : items) { + processItem(item); + } + } + for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) { + if(!(iter2.get() instanceof VisualizationTask)) { + continue; + } + VisualizationTask task = (VisualizationTask) iter2.get(); + boolean isprojected = false; + for(Hierarchy.Iter<?> iter = vistree.iterParents(task); iter.valid(); iter.advance()) { + if(iter.get() instanceof Projector) { + isprojected = true; + break; + } + } + if(isprojected) { + continue; + } + PlotItem pi = new PlotItem(ratio, 1.0, null); + pi.add(task); + processItem(pi); + } + } + + private void processItem(PlotItem item) { + // Descend into subitems + for(Iterator<PlotItem> iter = item.subitems.iterator(); iter.hasNext();) { + processItem(iter.next()); + } + if(item.taskSize() <= 0) { + return; + } + item.sort(); + final double width = item.w, height = item.h; + + VisualizationPlot svgp = new VisualizationPlot(); + svgp.getRoot().setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm"); + svgp.getRoot().setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, (20 * height / width) + "cm"); + svgp.getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 " + width + " " + height); + + ArrayList<Visualization> layers = new ArrayList<>(); + for(Iterator<VisualizationTask> iter = item.tasks.iterator(); iter.hasNext();) { + VisualizationTask task = iter.next(); + if(task.hasAnyFlags(VisualizationTask.FLAG_NO_DETAIL | VisualizationTask.FLAG_NO_EXPORT) || !task.visible) { + continue; + } + try { + Visualization v = task.getFactory().makeVisualization(task, svgp, width, height, item.proj); + layers.add(v); + } + catch(Exception e) { + if(Logging.getLogger(task.getFactory().getClass()).isDebugging()) { + LOG.exception("Visualization failed.", e); + } + else { + LOG.warning("Visualizer " + task.getFactory().getClass().getName() + " failed - enable debugging to see details."); + } + } + } + if(layers.size() <= 0) { + return; + } + for(Visualization layer : layers) { + if(layer.getLayer() == null) { + LOG.warning("NULL layer seen."); + continue; + } + svgp.getRoot().appendChild(layer.getLayer()); + } + svgp.updateStyleElement(); + + String prefix = null; + prefix = (prefix == null && item.proj != null) ? item.proj.getMenuName() : prefix; + prefix = (prefix == null && item.tasks.size() > 0) ? item.tasks.get(0).name : prefix; + prefix = (prefix != null ? prefix : "plot"); + // TODO: generate names... + Integer count = counter.get(prefix); + counter.put(prefix, count = count == null ? 1 : (count + 1)); + File outname = new File(output, prefix + "-" + count + ".svg"); + try { + svgp.saveAsSVG(outname); + } + catch(Exception e) { + LOG.warning("Export of visualization failed.", e); + } + for(Visualization layer : layers) { + layer.destroy(); + } + } + + /** + * Parameterization class + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Parameter to specify the canvas ratio + * <p> + * Key: {@code -vis.ratio} + * </p> + * <p> + * Default value: 1.33 + * </p> + */ + public static final OptionID RATIO_ID = new OptionID("vis.ratio", "The width/heigh ratio of the output."); + + /** + * Parameter to specify the output folder + * <p> + * Key: {@code -vis.output} + * </p> + */ + public static final OptionID FOLDER_ID = new OptionID("vis.output", "The output folder."); + + /** + * Visualization manager. + */ + VisualizerParameterizer manager; + + /** + * Output folder + */ + File output; + + /** + * Ratio for canvas + */ + double ratio; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + FileParameter outputP = new FileParameter(FOLDER_ID, FileType.OUTPUT_FILE); + if(config.grab(outputP)) { + output = outputP.getValue(); + } + + DoubleParameter ratioP = new DoubleParameter(RATIO_ID, 1.33); + ratioP.addConstraint(CommonConstraints.GREATER_THAN_ZERO_DOUBLE); + if(config.grab(ratioP)) { + ratio = ratioP.doubleValue(); + } + + manager = config.tryInstantiate(VisualizerParameterizer.class); + } + + @Override + protected ExportVisualizations makeInstance() { + return new ExportVisualizations(output, manager, ratio); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationItem.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationItem.java new file mode 100644 index 00000000..3f6c6f19 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationItem.java @@ -0,0 +1,38 @@ +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 <http://www.gnu.org/licenses/>. + */ + +/** + * Currently an empty interface for visualization items, that serves the purpose + * of improving type safety. + * + * @author Erich Schubert + */ +public interface VisualizationItem { + /** + * Name to display in the menu. May be {@code null} or empty string. + * + * @return Menu name. + */ + String getMenuName(); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationListener.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationListener.java new file mode 100644 index 00000000..4a8b3f03 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationListener.java @@ -0,0 +1,40 @@ +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 <http://www.gnu.org/licenses/>. + */ + +import java.util.EventListener; + +/** + * Listener for visualization events. + * + * @author Erich Schubert + */ +public interface VisualizationListener extends EventListener { + /** + * Visualization has changed. + * + * @param item Changed visualization + */ + void visualizationChanged(VisualizationItem item); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuAction.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuAction.java new file mode 100644 index 00000000..f16a208e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuAction.java @@ -0,0 +1,43 @@ +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 <http://www.gnu.org/licenses/>. + */ + +/** + * Visualizer actions. + * + * @author Erich Schubert + */ +public interface VisualizationMenuAction extends VisualizationItem { + /** + * Menu item was activated. + */ + void activate(); + + /** + * Indicate if the menu option is enabled or greyed out. + * + * @return {@code true} when enabled. + */ + boolean enabled(); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuToggle.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuToggle.java new file mode 100644 index 00000000..ad0ec9b3 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuToggle.java @@ -0,0 +1,50 @@ +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 <http://www.gnu.org/licenses/>. + */ + +/** + * Toggle action. + * + * @author Erich Schubert + */ +public interface VisualizationMenuToggle extends VisualizationItem { + /** + * Menu item was activated. + */ + void toggle(); + + /** + * Return the current state. + * + * @return {@code true} when selected. + */ + boolean active(); + + /** + * Indicate if the menu option is enabled or greyed out. + * + * @return {@code true} when enabled. + */ + boolean enabled(); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationProcessor.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationProcessor.java new file mode 100644 index 00000000..70271c1a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationProcessor.java @@ -0,0 +1,37 @@ +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 <http://www.gnu.org/licenses/>. + */ + +/** + * Visualization processor + * + * @author Erich Schubert + */ +public interface VisualizationProcessor { + /** + * Add visualization items for the given result (tree) to the result tree. + * @param context Visualization context + * @param start Result to process + */ + public void processNewResult(VisualizerContext context, Object start); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTask.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTask.java new file mode 100644 index 00000000..7be18864 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTask.java @@ -0,0 +1,316 @@ +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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory; + +/** + * Container class, with ugly casts to reduce generics crazyness. + * + * @author Erich Schubert + * + * @apiviz.landmark + * @apiviz.has VisualizerContext + * @apiviz.has VisFactory + */ +public class VisualizationTask implements VisualizationItem, Comparable<VisualizationTask> { + /** + * Meta data key: Level for visualizer ordering + * + * Returns an integer indicating the "height" of this Visualizer. It is + * intended to impose an ordering on the execution of Visualizers as a + * Visualizer may depend on another Visualizer running earlier. <br> + * Lower numbers should result in a earlier use of this Visualizer, while + * higher numbers should result in a later use. If more Visualizers have the + * same level, no ordering is guaranteed. <br> + * Note that this value is only a recommendation, as it is totally up to the + * framework to ignore it. + */ + public int level = 0; + + /** + * Flag to control visibility. + */ + public boolean visible = true; + + /** + * Capabilities + */ + private int flags; + + /** + * Flag to signal there is no thumbnail needed. + */ + public static final int FLAG_NO_THUMBNAIL = 1; + + /** + * Mark as not having a (sensible) detail view. + */ + public static final int FLAG_NO_DETAIL = 2; + + /** + * Flag to signal the visualizer should not be exported. + */ + public static final int FLAG_NO_EXPORT = 4; + + /** + * Flag to signal the visualizer should not be embedded. + */ + public static final int FLAG_NO_EMBED = 8; + + /** + * Flag to signal default visibility of a visualizer. + */ + public boolean default_visibility = true; + + /** + * Flag to mark the visualizer as tool. + */ + public boolean tool = false; + + /** + * Background layer + */ + public static final int LEVEL_BACKGROUND = 0; + + /** + * Data layer + */ + public static final int LEVEL_DATA = 100; + + /** + * Static plot layer + */ + public static final int LEVEL_STATIC = 200; + + /** + * Passive foreground layer + */ + public static final int LEVEL_FOREGROUND = 300; + + /** + * Active foreground layer (interactive elements) + */ + public static final int LEVEL_INTERACTIVE = 1000; + + /** + * The update event mask. See {@link #ON_DATA}, {@link #ON_SELECTION}, + * {@link #ON_STYLEPOLICY}, {@link #ON_SAMPLE}. + */ + public int updatemask; + + /** + * Constant to listen for data changes + */ + public static final int ON_DATA = 1; + + /** + * Constant to listen for selection changes + */ + public static final int ON_SELECTION = 2; + + /** + * Constant to listen for style result changes + */ + public static final int ON_STYLEPOLICY = 4; + + /** + * Constant to listen for sampling result changes + */ + public static final int ON_SAMPLE = 8; + + /** + * Name + */ + String name; + + /** + * The active context + */ + VisualizerContext context; + + /** + * The factory + */ + VisFactory factory; + + /** + * The result we are attached to + */ + Object result; + + /** + * The main representation + */ + Relation<?> relation; + + /** + * Width request + */ + public double reqwidth; + + /** + * Height request + */ + public double reqheight; + + /** + * Visualization task. + * + * @param name Name + * @param context Visualization context + * @param result Result + * @param relation Relation to use + * @param factory Factory + */ + public VisualizationTask(String name, VisualizerContext context, Object result, Relation<?> relation, VisFactory factory) { + super(); + this.name = name; + this.context = context; + this.result = result; + this.relation = relation; + this.factory = factory; + } + + /** + * Get the visualizer context. + * + * @return context + */ + public VisualizerContext getContext() { + return context; + } + + /** + * Get the visualizer factory. + * + * @return Visualizer factory + */ + public VisFactory getFactory() { + return factory; + } + + @SuppressWarnings("unchecked") + public <R> R getResult() { + return (R) result; + } + + @SuppressWarnings("unchecked") + public <R extends Relation<?>> R getRelation() { + return (R) relation; + } + + /** + * Init the default visibility of a task. + * + * @param vis Visibility. + */ + public void initDefaultVisibility(boolean vis) { + visible = vis; + default_visibility = vis; + } + + @Override + public String getMenuName() { + return name; + } + + @Override + public int compareTo(VisualizationTask other) { + // sort by levels first + if(this.level != other.level) { + return this.level - other.level; + } + // sort by name otherwise. + String name1 = this.getMenuName(); + String name2 = other.getMenuName(); + if(name1 != null && name2 != null && name1 != name2) { + return name1.compareTo(name2); + } + return 0; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("VisTask: ").append(factory.getClass().getName()).append(' '); + if(result != null && result instanceof Result) { + buf.append("Result: ").append(((Result) result).getLongName()).append(' '); + } + buf.append(super.toString()); + return buf.toString(); + } + + @Override + public int hashCode() { + // We can't have our hashcode change with the map contents! + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object o) { + // Also don't inherit equals based on list contents! + return (this == o); + } + + /** + * Set (OR) the update flags. + * + * @param bits Bits to set + */ + public void addUpdateFlags(int bits) { + updatemask |= bits; + } + + /** + * Update if any oft these bits is set. + * + * @param bits Bits to check. + * @return {@code true} if any bit is set. + */ + public boolean updateOnAny(int bits) { + return (updatemask & bits) != 0; + } + + /** + * Update if any oft these flags is set. + * + * @param bits Bits to check. + * @return {@code true} if any bit is set. + */ + public boolean hasAnyFlags(int bits) { + return (flags & bits) != 0; + } + + /** + * Set a task flag. + * + * @param bits Flag to set + */ + public void addFlags(int bits) { + flags |= bits; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTree.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTree.java new file mode 100644 index 00000000..8e76716c --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTree.java @@ -0,0 +1,395 @@ +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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; + +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.HashMapHierarchy; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.StackedIter; + +/** + * Tree - actually a forest - to manage visualizations. + * + * @author Erich Schubert + * + * @apiviz.has Handler1 + * @apiviz.has Handler2 + * @apiviz.has Handler3 + */ +public class VisualizationTree extends HashMapHierarchy<Object> { + /** + * The event listeners for this context. + */ + private ArrayList<VisualizationListener> vlistenerList = new ArrayList<>(); + + /** + * Constructor. + */ + public VisualizationTree() { + super(); + } + + /** + * Add a listener. + * + * @param listener Listener to add + */ + public void addVisualizationListener(VisualizationListener listener) { + for(int i = 0; i < vlistenerList.size(); i++) { + if(vlistenerList.get(i) == listener) { + return; + } + } + vlistenerList.add(listener); + } + + /** + * Add a listener. + * + * @param listener Listener to remove + */ + public void removeVisualizationListener(VisualizationListener listener) { + vlistenerList.remove(listener); + } + + /** + * A visualization item has changed. + * + * @param item Item that has changed + */ + public void visChanged(VisualizationItem item) { + for(int i = vlistenerList.size(); --i >= 0;) { + vlistenerList.get(i).visualizationChanged(item); + } + } + + /** + * Handler for a single result. + * + * @author Erich Schubert + * + * @param <A> Object type + */ + public static interface Handler1<A> { + /** + * Process a new result. + * + * @param context Context + * @param result First result + */ + void process(VisualizerContext context, A result); + } + + /** + * Handler for two result. + * + * @author Erich Schubert + * + * @param <A> Object type + * @param <B> Object type + */ + public static interface Handler2<A, B> { + /** + * Process a new result. + * + * @param context Context + * @param result First result + * @param result2 Second result + */ + void process(VisualizerContext context, A result, B result2); + } + + /** + * Handler for three result. + * + * @author Erich Schubert + * + * @param <A> Object type + * @param <B> Object type + * @param <C> Object type + */ + public static interface Handler3<A, B, C> { + /** + * Process a new result. + * + * @param context Context + * @param result First result + * @param result2 Second result + * @param result3 Third result + */ + void process(VisualizerContext context, A result, B result2, C result3); + } + + /** + * Filtered iteration over a stacked hierarchy. + * + * This is really messy because the visualization hierarchy is typed Object. + * + * @param context Visualization context + * @param clazz Type filter + * @param <O> Object type + * @return Iterator of results. + */ + @SuppressWarnings("unchecked") + public static <O extends VisualizationItem> Hierarchy.Iter<O> filter(VisualizerContext context, Class<? super O> clazz) { + Hierarchy.Iter<Result> it1 = context.getHierarchy().iterAll(); + StackedIter<Object, Result> it2 = new StackedIter<>(it1, context.getVisHierarchy()); + if(!it2.valid()) { + return HashMapHierarchy.emptyIterator(); + } + return new FilteredIter<O>(it2, (Class<O>) clazz); + } + + /** + * Filtered iteration over a stacked hierarchy. + * + * This is really messy because the visualization hierarchy is typed Object. + * + * @param context Visualization context + * @param start Starting object (in primary hierarchy!) + * @param clazz Type filter + * @param <O> Object type + * @return Iterator of results. + */ + @SuppressWarnings("unchecked") + public static <O extends VisualizationItem> Hierarchy.Iter<O> filter(VisualizerContext context, Object start, Class<? super O> clazz) { + if(start instanceof Result) { // In first hierarchy. + Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start); + StackedIter<Object, Result> it2 = new StackedIter<>(it1, context.getVisHierarchy()); + if(!it2.valid()) { + return HashMapHierarchy.emptyIterator(); + } + return new FilteredIter<O>(it2, (Class<O>) clazz); + } + Hierarchy.Iter<Object> it2 = context.getVisHierarchy().iterDescendantsSelf(start); + if(!it2.valid()) { + return HashMapHierarchy.emptyIterator(); + } + return new FilteredIter<O>(it2, (Class<O>) clazz); + } + + /** + * Filtered iteration over the primary result tree. + * + * @param context Visualization context + * @param start Starting object (in primary hierarchy!) + * @param clazz Type filter + * @param <O> Result type type + * @return Iterator of results. + */ + @SuppressWarnings("unchecked") + public static <O extends Result> Hierarchy.Iter<O> filterResults(VisualizerContext context, Object start, Class<? super O> clazz) { + if(start instanceof Result) { // In first hierarchy. + Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start); + return new FilteredIter<O>(it1, (Class<O>) clazz); + } + return HashMapHierarchy.emptyIterator(); + } + + /** + * Process new results. + * + * This is a bit painful, because we have two hierarchies with different + * types: results, and visualizations. + * + * @param context Context + * @param start Starting point + * @param type1 First type + * @param handler Handler + */ + @SuppressWarnings("unchecked") + public static <A> void findNew(VisualizerContext context, Object start, Class<? super A> type1, Handler1<A> handler) { + final Hierarchy<Object> hier = context.getVisHierarchy(); + // Children of start in first hierarchy: + if(start instanceof Result) { + Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start); + for(; it1.valid(); it1.advance()) { + final Result o1 = it1.get(); + if(!(type1.isInstance(o1))) { + continue; + } + handler.process(context, (A) o1); + } + } + // Children of start in second hierarchy: + if(start instanceof VisualizationItem) { + Iter<Object> it1 = hier.iterDescendantsSelf(start); + for(; it1.valid(); it1.advance()) { + final Object o1 = it1.get(); + if(!(type1.isInstance(o1))) { + continue; + } + handler.process(context, (A) start); + } + } + } + + /** + * Process new result combinations of an object type1 (in first hierarchy) and + * any child of type2 (in second hierarchy) + * + * This is a bit painful, because we have two hierarchies with different + * types: results, and visualizations. + * + * @param context Context + * @param start Starting point + * @param type1 First type, in first hierarchy + * @param type2 Second type, in second hierarchy + * @param handler Handler + */ + @SuppressWarnings("unchecked") + public static <A extends Result, B extends VisualizationItem> void findNewSiblings(VisualizerContext context, Object start, Class<? super A> type1, Class<? super B> type2, Handler2<A, B> handler) { + final Hierarchy<Object> vistree = context.getVisHierarchy(); + // Search start in first hierarchy: + if(start instanceof Result) { + Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start); + for(; it1.valid(); it1.advance()) { + final Result o1 = it1.get(); + if(!(type1.isInstance(o1))) { + continue; + } + Iter<Object> it2 = vistree.iterDescendantsSelf(context.getBaseResult()); + for(; it2.valid(); it2.advance()) { + final Object o2 = it2.get(); + if(!(type2.isInstance(o2))) { + continue; + } + handler.process(context, (A) o1, (B) o2); + } + } + } + // Search start in second hierarchy: + if(start instanceof VisualizationItem) { + Iter<Object> it2 = vistree.iterDescendantsSelf(start); + for(; it2.valid(); it2.advance()) { + final Object o2 = it2.get(); + if(!(type2.isInstance(o2))) { + continue; + } + Iter<Result> it1 = context.getHierarchy().iterAll(); + for(; it1.valid(); it1.advance()) { + final Result o1 = it1.get(); + if(!(type1.isInstance(o1))) { + continue; + } + handler.process(context, (A) o1, (B) o2); + } + } + } + } + + /** + * Process new result combinations of an object type1 (in first hierarchy) + * having a child of type2 (in second hierarchy). + * + * This is a bit painful, because we have two hierarchies with different + * types: results, and visualizations. + * + * @param context Context + * @param start Starting point + * @param type1 First type, in first hierarchy + * @param type2 Second type, in second hierarchy + * @param handler Handler + */ + @SuppressWarnings("unchecked") + public static <A extends Result, B extends VisualizationItem> void findNewResultVis(VisualizerContext context, Object start, Class<? super A> type1, Class<? super B> type2, Handler2<A, B> handler) { + final Hierarchy<Object> hier = context.getVisHierarchy(); + // Search start in first hierarchy: + if(start instanceof Result) { + Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start); + for(; it1.valid(); it1.advance()) { + final Result o1 = it1.get(); + if(!(type1.isInstance(o1))) { + continue; + } + // Nasty: we now need to search backwards for crossover points: + Iter<Result> it3 = context.getHierarchy().iterDescendantsSelf(o1); + for(; it3.valid(); it3.advance()) { + Iter<Object> it2 = hier.iterDescendantsSelf(it3.get()); + for(; it2.valid(); it2.advance()) { + final Object o2 = it2.get(); + if(!(type2.isInstance(o2))) { + continue; + } + handler.process(context, (A) o1, (B) o2); + } + } + } + } + // Search start in second hierarchy: + if(start instanceof VisualizationItem) { + Iter<Object> it2 = hier.iterDescendantsSelf(start); + for(; it2.valid(); it2.advance()) { + final Object o2 = it2.get(); + if(!(type2.isInstance(o2))) { + continue; + } + // Nasty: we now need to search backwards for crossover points: + Iter<Object> it3 = hier.iterAncestorsSelf(start); + for(; it3.valid(); it3.advance()) { + if(!(it3.get() instanceof Result)) { + continue; + } + // Now cross-over into primary hierarchy: + Iter<Result> it1 = context.getHierarchy().iterAncestorsSelf((Result) it3.get()); + for(; it1.valid(); it1.advance()) { + final Result o1 = it1.get(); + if(!(type1.isInstance(o1))) { + continue; + } + handler.process(context, (A) o1, (B) o2); + } + } + } + } + } + + /** + * Utility function to change Visualizer visibility. + * + * @param context Visualization context + * @param task Visualization task + * @param visibility Visibility value + */ + public static void setVisible(VisualizerContext context, VisualizationTask task, boolean visibility) { + // Hide other tools + if(visibility && task.tool) { + Hierarchy<Object> vistree = context.getVisHierarchy(); + for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) { + if(!(iter2.get() instanceof VisualizationTask)) { + continue; + } + VisualizationTask other = (VisualizationTask) iter2.get(); + if(other != task && other.tool && other.visible) { + other.visible = false; + context.visChanged(other); + } + } + } + task.visible = visibility; + context.visChanged(task); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerContext.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerContext.java new file mode 100644 index 00000000..8d288ce6 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerContext.java @@ -0,0 +1,452 @@ +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 <http://www.gnu.org/licenses/>. + */ + +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.logging.Logging; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultHierarchy; +import de.lmu.ifi.dbs.elki.result.ResultListener; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.SelectionResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +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 + * + * @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; + + /** + * Relation currently visualized. + */ + private Relation<?> relation; + + /** + * Constructor. We currently require a Database and a Result. + * + * @param hier Result hierarchy + * @param start Starting result + * @param factories Visualizer Factories to use + */ + public VisualizerContext(ResultHierarchy hier, Result start, Relation<?> relation, 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; + } + ResultUtil.ensureClusteringResult(db, db); + this.selection = ResultUtil.ensureSelectionResult(db); + for(Relation<?> rel : ResultUtil.getRelations(db)) { + ResultUtil.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; + ResultUtil.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 = ResultUtil.getClusteringResults(db); + if(clusterings.size() > 0) { + 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); + } + + /** + * Current relation. + */ + public Relation<?> getRelation() { + return relation; + } + + /** + * Set the current relation. + * + * @param rel Relation + */ + public void setRelation(Relation<?> rel) { + this.relation = rel; + getHierarchy().resultChanged(this); + } + + /** + * 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<>(); + for(Hierarchy.Iter<?> iter = vistree.iterDescendants(item); iter.valid(); iter.advance()) { + Object o = iter.get(); + if(o instanceof VisualizationTask) { + out.add((VisualizationTask) o); + } + } + return out; + } + + public VisualizationTree getVisHierarchy() { + return vistree; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerParameterizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerParameterizer.java new file mode 100644 index 00000000..df3d99de --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerParameterizer.java @@ -0,0 +1,354 @@ +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 <http://www.gnu.org/licenses/>. + */ + +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 <em>really</em> 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 + * + * @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<VisualizationProcessor> 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<VisualizationProcessor> 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<Relation<?>> 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<TrackedParameter> 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. + * + * <p> + * Key: -visualizer.stylesheet + * + * Default: default properties file <br> + * included stylesheets: + * <ul> + * <li>classic</li> + * <li>default</li> + * <li>greyscale</li> + * <li>neon</li> + * <li>presentation</li> + * <li>print</li> + * </ul> + * These are {@code *.properties} files in the package + * {@link de.lmu.ifi.dbs.elki.visualization.style}. + * </p> + */ + 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 + * + * <p> + * Key: -vis.enable + * + * Default: ELKI core + * </p> + */ + public static final OptionID ENABLEVIS_ID = new OptionID("vis.enable", "Visualizers to enable by default."); + + /** + * Parameter to set the sampling level + * + * <p> + * Key: -vis.sampling + * </p> + */ + 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<VisualizationProcessor> 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 <O> Collection<VisualizationProcessor> collectFactorys(MergedParameterization config, Pattern filter) { + ArrayList<VisualizationProcessor> 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); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AddCSSClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AddCSSClass.java new file mode 100755 index 00000000..e32d84a3 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AddCSSClass.java @@ -0,0 +1,62 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + + +/** + * Add a CSS class to the event target. + * + * @author Erich Schubert + * + */ +public class AddCSSClass implements EventListener { + /** + * Class to set + */ + private String cssclass; + + /** + * Constructor + * @param cssclass class to set + */ + public AddCSSClass(String cssclass) { + super(); + this.cssclass = cssclass; + } + + /** + * Event handler + */ + @Override + public void handleEvent(Event evt) { + Element e = (Element) evt.getTarget(); + SVGUtil.addCSSClass(e, cssclass); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AttributeModifier.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AttributeModifier.java new file mode 100644 index 00000000..21409018 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AttributeModifier.java @@ -0,0 +1,74 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +/** + * Runnable wrapper for modifying XML-Attributes. + * + * @author Remigius Wojdanowski + * + */ +// FIXME: Unused? Remove? +public class AttributeModifier implements Runnable { + + /** + * Provides the attribute to be modified. + */ + private Element e; + + /** + * The name of the attribute to be modified. + */ + private String attribute; + + /** + * The new value of the attribute. + */ + private String newValue; + + /** + * Trivial constructor. + * + * @param e provides the attribute to be modified. + * @param attribute the name of the attribute to be modified. + * @param newValue the new value of the attribute. + */ + public AttributeModifier(Element e, String attribute, String newValue) { + this.e = e; + this.attribute = attribute; + this.newValue = newValue; + } + + @Override + public void run() { + if(newValue != null) { + e.setAttribute(attribute, newValue); + } + else { + e.removeAttribute(attribute); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/BatikUtil.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/BatikUtil.java new file mode 100644 index 00000000..44eb38d6 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/BatikUtil.java @@ -0,0 +1,65 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.dom.events.DOMMouseEvent; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGElement; +import org.w3c.dom.svg.SVGLocatable; +import org.w3c.dom.svg.SVGMatrix; +import org.w3c.dom.svg.SVGPoint; + +/** + * Batik helper class with static methods. + * + * @author Erich Schubert + */ +public final class BatikUtil { + /** + * Get the relative coordinates of a point within the coordinate system of a + * particular SVG Element. + * + * @param evt Event, needs to be a DOMMouseEvent + * @param reference SVG Element the coordinate system is used of + * @return Array containing the X and Y values + */ + public static double[] getRelativeCoordinates(Event evt, Element reference) { + if(evt instanceof DOMMouseEvent && reference instanceof SVGLocatable && reference instanceof SVGElement) { + // Get the screen (pixel!) coordinates + DOMMouseEvent gnme = (DOMMouseEvent) evt; + SVGMatrix mat = ((SVGLocatable) reference).getScreenCTM(); + SVGMatrix imat = mat.inverse(); + SVGPoint cPt = ((SVGElement) reference).getOwnerSVGElement().createSVGPoint(); + cPt.setX(gnme.getClientX()); + cPt.setY(gnme.getClientY()); + // Have Batik transform the screen (pixel!) coordinates into SVG element + // coordinates + cPt = cPt.matrixTransform(imat); + + return new double[] { cPt.getX(), cPt.getY() }; + } + return null; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CSSHoverClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CSSHoverClass.java new file mode 100755 index 00000000..7140b73f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CSSHoverClass.java @@ -0,0 +1,110 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Do a hover effect using a CSS class. + * + * @author Erich Schubert + * + */ +public class CSSHoverClass implements EventListener { + /** + * Class to set when over + */ + private String overclass; + + /** + * Class to set when out + */ + private String outclass; + + /** + * Consider a click as 'out'? + */ + private boolean clickisout; + + /** + * Constructor + * + * @param overclass class to set when over + * @param outclass class to set when out + * @param clickisout consider a click to be an 'out' event + */ + public CSSHoverClass(String overclass, String outclass, boolean clickisout) { + super(); + this.overclass = overclass; + this.outclass = outclass; + this.clickisout = clickisout; + } + + /** + * Constructor without 'clickisout' option. + * + * @param overclass class to set when over + * @param outclass class to set when out + */ + public CSSHoverClass(String overclass, String outclass) { + this(overclass, outclass, false); + } + + /** + * Event handler + */ + @Override + public void handleEvent(Event evt) { + Element e = (Element) evt.getTarget(); + if (SVGConstants.SVG_EVENT_MOUSEOVER.equals(evt.getType())) { + if (overclass != null) { + SVGUtil.addCSSClass(e, overclass); + } + if (outclass != null) { + SVGUtil.removeCSSClass(e, outclass); + } + } + if (SVGConstants.SVG_EVENT_MOUSEOUT.equals(evt.getType())) { + if (overclass != null) { + SVGUtil.removeCSSClass(e, overclass); + } + if (outclass != null) { + SVGUtil.addCSSClass(e, outclass); + } + } + if (clickisout && SVGConstants.SVG_EVENT_CLICK.equals(evt.getType())) { + if (overclass != null) { + SVGUtil.removeCSSClass(e, overclass); + } + if (outclass != null) { + SVGUtil.addCSSClass(e, outclass); + } + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CloneInlineImages.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CloneInlineImages.java new file mode 100644 index 00000000..93033f01 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CloneInlineImages.java @@ -0,0 +1,144 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.renderable.RenderableImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.apache.batik.svggen.SVGSyntax; +import org.apache.batik.util.Base64EncoderStream; +import org.apache.batik.util.ParsedURL; +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGCloneVisible; + +/** + * Clone an SVG document, inlining temporary and in-memory linked images. + * + * @author Erich Schubert + * + * @apiviz.has ThumbnailRegistryEntry + */ +public class CloneInlineImages extends SVGCloneVisible { + @Override + public Node cloneNode(Document doc, Node eold) { + Node enew = null; + if(eold instanceof Element) { + Element e = (Element) eold; + if(e.getTagName().equals(SVGConstants.SVG_IMAGE_TAG)) { + String url = e.getAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE); + ParsedURL urldata = new ParsedURL(url); + if(ThumbnailRegistryEntry.isCompatibleURLStatic(urldata)) { + enew = inlineThumbnail(doc, urldata, eold); + } + else if("file".equals(urldata.getProtocol())) { + enew = inlineExternal(doc, urldata, eold); + } + } + } + if(enew != null) { + return enew; + } + return super.cloneNode(doc, eold); + } + + /** + * Inline a referenced thumbnail. + * + * @param doc Document (element factory) + * @param urldata URL + * @param eold Existing node + * @return Replacement node, or {@code null} + */ + protected Node inlineThumbnail(Document doc, ParsedURL urldata, Node eold) { + RenderableImage img = ThumbnailRegistryEntry.handleURL(urldata); + if(img == null) { + LoggingUtil.warning("Image not found in registry: " + urldata.toString()); + return null; + } + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + os.write(SVGSyntax.DATA_PROTOCOL_PNG_PREFIX.getBytes()); + Base64EncoderStream encoder = new Base64EncoderStream(os); + ImageIO.write(img.createDefaultRendering(), "png", encoder); + encoder.close(); + } + catch(IOException e) { + LoggingUtil.exception("Exception serializing image to png", e); + return null; + } + Element i = (Element) super.cloneNode(doc, eold); + i.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE, os.toString().replaceAll("\\s*[\\r\\n]+\\s*", "")); + return i; + } + + /** + * Inline an external file (usually from temp). + * + * @param doc Document (element factory) + * @param urldata URL + * @param eold Existing node + * @return Replacement node, or {@code null} + */ + protected Node inlineExternal(Document doc, ParsedURL urldata, Node eold) { + File in = new File(urldata.getPath()); + if(!in.exists()) { + LoggingUtil.warning("Referencing non-existant file: " + urldata.toString()); + return null; + } + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + os.write(SVGSyntax.DATA_PROTOCOL_PNG_PREFIX.getBytes()); + Base64EncoderStream encoder = new Base64EncoderStream(os); + FileInputStream instream = new FileInputStream(in); + byte[] buf = new byte[4096]; + while(true) { + int read = instream.read(buf, 0, buf.length); + if(read <= 0) { + break; + } + encoder.write(buf, 0, read); + } + instream.close(); + encoder.close(); + } + catch(IOException e) { + LoggingUtil.exception("Exception serializing image to png", e); + return null; + } + + Element i = (Element) super.cloneNode(doc, eold); + i.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE, os.toString().replaceAll("\\s*[\\r\\n]+\\s*", "")); + return i; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/DragableArea.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/DragableArea.java new file mode 100644 index 00000000..dc5dd388 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/DragableArea.java @@ -0,0 +1,368 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * A simple dragable area for Batik. + * + * @author Erich Schubert + * + * @apiviz.has DragListener + * @apiviz.has Element + */ +public class DragableArea implements EventListener { + /** + * Our element node. + */ + final protected Element element; + + /** + * The coordinate system node. + */ + final protected Element coordref; + + /** + * The plot we are attached to. + */ + final protected SVGPlot svgp; + + /** + * The point where the drag started. + */ + protected SVGPoint startDragPoint = null; + + /** + * A listener to notify on drags (when not subclassing). + */ + protected DragListener listener = null; + + /** + * Constructor for a dragable area. use getElement() to get the DOM node. + * + * Note: always remember to call 'destroy()' to remove listeners! + * + * @param plot Plot we'll be added to + * @param x X position + * @param y Y position + * @param w Width + * @param h Height + */ + public DragableArea(SVGPlot plot, double x, double y, double w, double h) { + this.svgp = plot; + this.element = plot.svgRect(x, y, w, h); + makeInvisible(); + this.coordref = this.element; + enableStart(); + } + + /** + * Constructor for a dragable area. use getElement() to get the DOM node. + * + * Note: always remember to call 'destroy()' to remove listeners! + * + * @param plot Plot we'll be added to + * @param coordref Element defining the coordinate system + * @param x X position + * @param y Y position + * @param w Width + * @param h Height + */ + public DragableArea(SVGPlot plot, Element coordref, double x, double y, double w, double h) { + this.svgp = plot; + this.element = plot.svgRect(x, y, w, h); + makeInvisible(); + this.coordref = coordref; + enableStart(); + } + + /** + * Constructor for a dragable area. use getElement() to get the DOM node. + * + * Note: always remember to call 'destroy()' to remove listeners! + * + * @param plot Plot we'll be added to + * @param x X position + * @param y Y position + * @param w Width + * @param h Height + * @param listener Drag listener + */ + public DragableArea(SVGPlot plot, double x, double y, double w, double h, DragListener listener) { + this.svgp = plot; + this.element = plot.svgRect(x, y, w, h); + makeInvisible(); + this.coordref = this.element; + this.listener = listener; + enableStart(); + } + + /** + * Constructor for a dragable area. use getElement() to get the DOM node. + * + * Note: always remember to call 'destroy()' to remove listeners! + * + * @param plot Plot we'll be added to + * @param coordref Element defining the coordinate system + * @param x X position + * @param y Y position + * @param w Width + * @param h Height + * @param listener Drag listener + */ + public DragableArea(SVGPlot plot, Element coordref, double x, double y, double w, double h, DragListener listener) { + this.svgp = plot; + this.element = plot.svgRect(x, y, w, h); + makeInvisible(); + this.coordref = coordref; + this.listener = listener; + enableStart(); + } + + /** + * Remove the listeners + */ + public void destroy() { + disableStart(); + disableStop(); + } + + /** + * The DOM element. + * + * @return the element + */ + public Element getElement() { + return element; + } + + /** + * Enable capturing of 'mousedown' events. + */ + public void enableStart() { + EventTarget targ = (EventTarget) element; + targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEDOWN, this, false); + } + + /** + * Disable capturing of 'mousedown' events. + */ + public void disableStart() { + EventTarget targ = (EventTarget) element; + targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEDOWN, this, false); + } + + /** + * Enable capturing of 'mousemove' and 'mouseup' events. + */ + protected void enableStop() { + EventTarget targ = svgp.getDocument().getRootElement(); + targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEMOVE, this, false); + targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEUP, this, false); + // FIXME: listen on the background object! + targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEOUT, this, false); + } + + /** + * Disable capturing of 'mousemove' and 'mouseup' events. + */ + protected void disableStop() { + EventTarget targ = svgp.getDocument().getRootElement(); + targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEMOVE, this, false); + targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEUP, this, false); + // FIXME: listen on the background object! + targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEOUT, this, false); + } + + @Override + public void handleEvent(Event evt) { + if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEDOWN)) { + SVGPoint dragPoint = getCoordinates(evt); + if (startDrag(dragPoint, evt)) { + // LoggingUtil.warning("Starting drag: "+dragPoint); + startDragPoint = dragPoint; + enableStop(); + } + } else if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEMOVE)) { + if (startDragPoint != null) { + SVGPoint dragPoint = getCoordinates(evt); + if (!duringDrag(startDragPoint, dragPoint, evt, evt.getTarget() == element)) { + // cancel the drag operation + startDragPoint = null; + disableStop(); + } + } + } else if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEUP)) { + if (startDragPoint != null) { + SVGPoint dragPoint = getCoordinates(evt); + if (endDrag(startDragPoint, dragPoint, evt, evt.getTarget() == element)) { + // LoggingUtil.warning("Drag completed: "+dragPoint); + startDragPoint = null; + disableStop(); + } + } + } else if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEOUT)) { + // When leaving the document with the mouse! + if (startDragPoint != null && evt.getTarget() == evt.getCurrentTarget()) { + // LoggingUtil.warning("Mouseout: "+evt.getTarget().toString()); + SVGPoint dragPoint = getCoordinates(evt); + if (endDrag(startDragPoint, dragPoint, evt, false)) { + // LoggingUtil.warning("Drag completed: "+dragPoint); + startDragPoint = null; + disableStop(); + } + } + } else { + LoggingUtil.warning("Unrecognized event: " + evt); + } + } + + /** + * Return the event coordinates for this event. + * + * @param evt Event + * @return Coordinates + */ + protected SVGPoint getCoordinates(Event evt) { + return SVGUtil.elementCoordinatesFromEvent(this.svgp.getDocument(), this.coordref, evt); + } + + /** + * Action to do on drag start. + * + * @param startPoint Point where the drag was started. + * @param evt The event object + * @return {@code true} to start the drag operation + */ + protected boolean startDrag(SVGPoint startPoint, Event evt) { + if (listener != null) { + return listener.startDrag(startPoint, evt); + } + return true; + } + + /** + * Method called during drags. + * + * @param startPoint Drag starting point + * @param dragPoint Drag end point + * @param evt The event object + * @param inside Inside the tracked element + * @return {@code true} to continue the drag + */ + protected boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + if (listener != null) { + return listener.duringDrag(startPoint, dragPoint, evt, inside); + } + return true; + } + + /** + * Method called when a drag was ended. + * + * @param startPoint Drag starting point + * @param dragPoint Drag end point + * @param evt The event object + * @param inside Success flag + * @return {@code true} to complete the drag + */ + protected boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + if (listener != null) { + return listener.endDrag(startPoint, dragPoint, evt, inside); + } + return true; + } + + /** + * Make the rectangle invisible. + */ + public void makeInvisible() { + CSSClass cls = new CSSClass(this, "unused"); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0"); + cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + SVGUtil.setAtt(element, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS()); + } + + /** + * Make the rectangle visible, for debug purposes. + */ + public void makeVisible() { + CSSClass cls = new CSSClass(this, "unused"); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREEN_VALUE); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0.2"); + cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + SVGUtil.setAtt(element, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS()); + } + + /** + * Listener interface for drag events. + * + * @author Erich Schubert + * + * @apiviz.excludeSubtypes + */ + public interface DragListener { + /** + * Action to do on drag start. + * + * @param startPoint Point where the drag was started. + * @param evt The event object + * @return {@code true} to start the drag operation + */ + boolean startDrag(SVGPoint startPoint, Event evt); + + /** + * Method called during drags. + * + * @param startPoint Drag starting point + * @param dragPoint Drag end point + * @param evt The event object + * @param inside Inside the tracked element + * @return {@code true} to continue the drag + */ + boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside); + + /** + * Method called when a drag was ended. + * + * @param startPoint Drag starting point + * @param dragPoint Drag end point + * @param evt The event object + * @param inside Whether the end point was inside the area + * @return {@code true} to complete the drag + */ + boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGSynchronizedCanvas.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGSynchronizedCanvas.java new file mode 100644 index 00000000..2f9ac431 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGSynchronizedCanvas.java @@ -0,0 +1,171 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.batik.bridge.UpdateManager; +import org.apache.batik.swing.JSVGCanvas; +import org.w3c.dom.Document; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * An JSVGCanvas that allows easier synchronization of Updates for SVGPlot + * objects. + * + * @author Erich Schubert + * + * @apiviz.composedOf JSVGUpdateSynchronizer + * @apiviz.has SVGPlot oneway - - displays + */ +public class JSVGSynchronizedCanvas extends JSVGCanvas { + /** + * Serial version number. + */ + private static final long serialVersionUID = 1L; + + /** + * Synchronizer to use when synchronizing SVG plots + */ + final private JSVGUpdateSynchronizer synchronizer; + + /** + * Current SVG plot. + */ + private SVGPlot plot = null; + + /** + * The latest attaching operation. + */ + private final AtomicReference<Runnable> latest = new AtomicReference<>(); + + /** + * Constructor + */ + public JSVGSynchronizedCanvas() { + super(); + this.synchronizer = new JSVGUpdateSynchronizer(this); + super.setDocumentState(JSVGCanvas.ALWAYS_DYNAMIC); + } + + /** + * Get the currently displayed SVG plot. + * + * @return current SVG plot. May be {@code null}! + */ + public SVGPlot getPlot() { + return this.plot; + } + + /** + * Use {@link #setPlot} instead if you need synchronization! + * + * @deprecated Document cannot be synchronized - use {@link #setPlot} and a + * {@link SVGPlot} object! + */ + @Override + @Deprecated + public synchronized void setDocument(Document doc) { + // Note: this will call this.setSVGDocument! + super.setDocument(doc); + } + + /** + * Choose a new plot to display. + * + * @param newplot New plot to display. May be {@code null}! + */ + public void setPlot(final SVGPlot newplot) { + synchronized(synchronizer) { + super.setSVGDocument(null); + scheduleSetPlot(this.plot, newplot); + } + } + + /** + * Schedule a detach. + * + * @param oldplot Plot to detach from. + */ + private void scheduleSetPlot(final SVGPlot oldplot, final SVGPlot newplot) { + UpdateManager um = this.getUpdateManager(); + if(um != null) { + synchronized(um) { + if(um.isRunning()) { + // LoggingUtil.warning("Scheduling detach: " + this + " " + oldplot); + final Runnable detach = new Runnable() { + @Override + public void run() { + if(latest.compareAndSet(this, null)) { + detachPlot(oldplot); + attachPlot(newplot); + } + } + }; + latest.set(detach); + um.getUpdateRunnableQueue().preemptLater(detach); + return; + } + } + } + else { + if(oldplot != null) { + LoggingUtil.warning("No update manager, but a previous plot exists. Incorrectly initialized?"); + } + } + detachPlot(oldplot); + attachPlot(newplot); + } + + /** + * Attach to a new plot, and display. + * + * @param newplot Plot to attach to. + */ + private void attachPlot(SVGPlot newplot) { + this.plot = newplot; + if(newplot == null) { + super.setSVGDocument(null); + return; + } + newplot.synchronizeWith(synchronizer); + super.setSVGDocument(newplot.getDocument()); + super.setDisableInteractions(newplot.getDisableInteractions()); + } + + /** + * Execute the detaching event. + * + * @param oldplot Plot to detach from. + */ + private void detachPlot(SVGPlot oldplot) { + if(oldplot == null) { + return; + } + this.plot = null; + oldplot.unsynchronizeWith(JSVGSynchronizedCanvas.this.synchronizer); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGUpdateSynchronizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGUpdateSynchronizer.java new file mode 100644 index 00000000..6fe43df1 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGUpdateSynchronizer.java @@ -0,0 +1,184 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.lang.ref.WeakReference; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.batik.bridge.UpdateManager; +import org.apache.batik.bridge.UpdateManagerAdapter; +import org.apache.batik.bridge.UpdateManagerEvent; +import org.apache.batik.swing.svg.JSVGComponent; + +import de.lmu.ifi.dbs.elki.visualization.svg.UpdateRunner; +import de.lmu.ifi.dbs.elki.visualization.svg.UpdateSynchronizer; + +/** + * This class is used to synchronize SVG updates with an JSVG canvas. + * + * @author Erich Schubert + * + * @apiviz.uses UpdateRunner + */ +class JSVGUpdateSynchronizer implements UpdateSynchronizer { + /** + * A weak reference to the component the plot is in. + */ + private final WeakReference<JSVGComponent> cref; + + /** + * The UpdateRunner we are put into + */ + private Set<WeakReference<UpdateRunner>> updaterunner = new CopyOnWriteArraySet<>(); + + /** + * Adapter to track component changes + */ + private final UMAdapter umadapter = new UMAdapter(); + + /** + * The current Runnable scheduled, prevents repeated invocations. + */ + private final AtomicReference<Runnable> pending = new AtomicReference<>(); + + /** + * Create an updateSynchronizer for the given component. + * + * @param component Component to manage updates on. + */ + protected JSVGUpdateSynchronizer(JSVGComponent component) { + assert(component != null); + + this.cref = new WeakReference<>(component); + // Hook into UpdateManager creation. + component.addUpdateManagerListener(umadapter); + // makeRunnerIfNeeded(); + } + + @Override + public void activate() { + makeRunnerIfNeeded(); + } + + /** + * Join the runnable queue of a component. + */ + protected void makeRunnerIfNeeded() { + // We don't need to make a SVG runner when there are no pending updates. + boolean stop = true; + for(WeakReference<UpdateRunner> wur : updaterunner) { + UpdateRunner ur = wur.get(); + if(ur == null) { + updaterunner.remove(wur); + } + else if(!ur.isEmpty()) { + stop = false; + } + } + if(stop) { + return; + } + // We only need a new runner when we don't have one in the queue yet! + if(pending.get() != null) { + return; + } + // We need a component + JSVGComponent component = this.cref.get(); + if(component == null) { + return; + } + // Synchronize with all layers: + synchronized(this) { + synchronized(component) { + UpdateManager um = component.getUpdateManager(); + if(um != null) { + synchronized(um) { + if(um.isRunning()) { + // Create and insert a runner. + Runnable newrunner = new Runnable() { + @Override + public void run() { + if(pending.compareAndSet(this, null)) { + // Wake up all runners + for(WeakReference<UpdateRunner> wur : updaterunner) { + UpdateRunner ur = wur.get(); + if(ur == null || ur.isEmpty()) { + continue; + } + ur.runQueue(); + } + } + } + }; + pending.set(newrunner); + um.getUpdateRunnableQueue().invokeLater(newrunner); + return; + } + } + } + } + } + } + + @Override + public void addUpdateRunner(UpdateRunner updateRunner) { + for(WeakReference<UpdateRunner> wur : updaterunner) { + if(wur.get() == null) { + updaterunner.remove(wur); + } + } + updaterunner.add(new WeakReference<>(updateRunner)); + } + + /** + * Adapter that will track the component for UpdateManager changes. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + private class UMAdapter extends UpdateManagerAdapter { + /** + * Constructor. Protected to allow construction above. + */ + protected UMAdapter() { + // nothing to do. + } + + /** + * React to an update manager becoming available. + */ + @Override + public void managerStarted(UpdateManagerEvent e) { + makeRunnerIfNeeded(); + } + + @Override + public void managerStopped(UpdateManagerEvent e) { + pending.set(null); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/LazyCanvasResizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/LazyCanvasResizer.java new file mode 100644 index 00000000..c180d329 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/LazyCanvasResizer.java @@ -0,0 +1,116 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.Component; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; + +/** + * Class to lazily process canvas resize events by applying a threshold. + * + * @author Erich Schubert + */ +public abstract class LazyCanvasResizer extends ComponentAdapter { + /** + * Default threshold for resizing. + */ + public static final double DEFAULT_THRESHOLD = 0.05; + + /** + * Active threshold + */ + double threshold; + + /** + * Last ratio of the Canvas applied + */ + double activeRatio; + + /** + * Component the ratio applies to. + */ + Component component; + + /** + * Full constructor. + * + * @param component Component to track + * @param threshold Threshold + */ + public LazyCanvasResizer(Component component, double threshold) { + super(); + this.threshold = threshold; + this.component = component; + this.activeRatio = getCurrentRatio(); + } + + /** + * Simplified constructor using the default threshold {@link #DEFAULT_THRESHOLD} + * + * @param component Component to track. + */ + public LazyCanvasResizer(Component component) { + this(component, DEFAULT_THRESHOLD); + } + + /** + * React to a component resize event. + */ + @Override + public void componentResized(ComponentEvent e) { + if (e.getComponent() == component) { + double newRatio = getCurrentRatio(); + if (Math.abs(newRatio - activeRatio) > threshold) { + activeRatio = newRatio; + executeResize(newRatio); + } + } + } + + /** + * Get the components current ratio. + * + * @return Current ratio. + */ + public final double getCurrentRatio() { + return (double) component.getWidth() / (double) component.getHeight(); + } + + /** + * Callback function that needs to be overridden with actual implementations. + * + * @param newratio New ratio to apply. + */ + public abstract void executeResize(double newratio); + + /** + * Get the components last applied ratio. + * + * @return Last applied ratio. + */ + public double getActiveRatio() { + return activeRatio; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeAppendChild.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeAppendChild.java new file mode 100644 index 00000000..79bdab54 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeAppendChild.java @@ -0,0 +1,89 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * Runnable wrapper for appending XML-Elements to existing Elements. + * + * @author Remigius Wojdanowski + */ +public class NodeAppendChild implements Runnable { + /** + * Parent node to append to. + */ + protected Element parent; + + /** + * The child to be appended. + */ + protected Element child; + + /** + * The plot (for ID updates). May be {@code null}. + */ + protected SVGPlot plot; + + /** + * The ID. May be {code null}. + */ + protected String id; + + /** + * Trivial constructor. + * + * @param parent will become the parent of the appended Element. + * @param child the Element to be appended. + */ + public NodeAppendChild(Element parent, Element child) { + this(parent, child, null, null); + } + + /** + * Full constructor. + * + * @param parent Parent node to append the child to + * @param child Child element + * @param plot Plot to register the ID (may be {@code null}) + * @param id ID to register (may be {@code null}, requires plot to be given) + */ + public NodeAppendChild(Element parent, Element child, SVGPlot plot, String id) { + super(); + this.parent = parent; + this.child = child; + this.plot = plot; + this.id = id; + } + + @Override + public void run() { + parent.appendChild(child); + if(plot != null && id != null) { + plot.putIdElement(id, child); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceAllChildren.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceAllChildren.java new file mode 100644 index 00000000..45f2b25d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceAllChildren.java @@ -0,0 +1,66 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * Runnable wrapper to replace all children of a given node. + * + * @author Erich Schubert + */ +public class NodeReplaceAllChildren extends NodeAppendChild { + /** + * Trivial constructor. + * + * @param parent will become the parent of the appended Element. + * @param child the Element to be appended. + */ + public NodeReplaceAllChildren(Element parent, Element child) { + super(parent, child, null, null); + } + + /** + * Full constructor. + * + * @param parent Parent node to append the child to + * @param child Child element + * @param plot Plot to register the ID (may be {@code null}) + * @param id ID to register (may be {@code null}, requires plot to be given) + */ + public NodeReplaceAllChildren(Element parent, Element child, SVGPlot plot, String id) { + super(parent, child, plot, id); + } + + @Override + public void run() { + // remove all existing children. + while(parent.hasChildNodes()) { + parent.removeChild(parent.getLastChild()); + } + super.run(); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceByID.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceByID.java new file mode 100644 index 00000000..27ac9672 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceByID.java @@ -0,0 +1,75 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * This helper class will replace a node in an SVG plot. This is a Runnable to + * be put on the update queue. + * + * @author Erich Schubert + */ +public class NodeReplaceByID implements Runnable { + /** + * Plot to work in. + */ + private SVGPlot plot; + + /** + * ID of element to replace. + */ + private String id; + + /** + * Replacement element. + */ + private Element newe; + + /** + * Setup a SVG node replacement. + * + * @param newe New element + * @param plot SVG plot to process + * @param id Node ID to replace + */ + public NodeReplaceByID(Element newe, SVGPlot plot, String id) { + super(); + this.newe = newe; + this.plot = plot; + this.id = id; + } + + @Override + public void run() { + Element olde = plot.getIdElement(id); + if(olde != null) { + olde.getParentNode().replaceChild(newe, olde); + plot.putIdElement(id, newe); + } + // Note: no warning if it is not possible! + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeSubstitute.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeSubstitute.java new file mode 100644 index 00000000..bef276dc --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeSubstitute.java @@ -0,0 +1,62 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +/** + * This helper class will replace a node in an SVG plot. This is a Runnable to + * be put on the update queue. + * + * @author Erich Schubert + */ +public class NodeSubstitute implements Runnable { + /** + * Existing element to replace. + */ + private Element prev; + + /** + * Replacement element. + */ + private Element newe; + + /** + * Setup a SVG node replacement. + * + * @param prev Existing element + * @param newe New element + */ + public NodeSubstitute(Element prev, Element newe) { + super(); + this.prev = prev; + this.newe = newe; + } + + @Override + public void run() { + prev.getParentNode().replaceChild(newe, prev); + // Note: no warning if it is not possible! + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/RemoveCSSClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/RemoveCSSClass.java new file mode 100755 index 00000000..c7d48690 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/RemoveCSSClass.java @@ -0,0 +1,62 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + + +/** + * Remove a CSS class to the event target. + * + * @author Erich Schubert + * + */ +public class RemoveCSSClass implements EventListener { + /** + * Class to set + */ + private String cssclass; + + /** + * Constructor + * @param cssclass class to set + */ + public RemoveCSSClass(String cssclass) { + super(); + this.cssclass = cssclass; + } + + /** + * Event handler + */ + @Override + public void handleEvent(Event evt) { + Element e = (Element) evt.getTarget(); + SVGUtil.removeCSSClass(e, cssclass); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailRegistryEntry.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailRegistryEntry.java new file mode 100644 index 00000000..3f1c5cda --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailRegistryEntry.java @@ -0,0 +1,251 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import gnu.trove.iterator.TIntObjectIterator; +import gnu.trove.map.TIntObjectMap; +import gnu.trove.map.hash.TIntObjectHashMap; + +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.util.Iterator; + +import org.apache.batik.ext.awt.image.GraphicsUtil; +import org.apache.batik.ext.awt.image.renderable.Filter; +import org.apache.batik.ext.awt.image.renderable.RedRable; +import org.apache.batik.ext.awt.image.spi.AbstractRegistryEntry; +import org.apache.batik.ext.awt.image.spi.ImageTagRegistry; +import org.apache.batik.ext.awt.image.spi.MagicNumberRegistryEntry; +import org.apache.batik.ext.awt.image.spi.URLRegistryEntry; +import org.apache.batik.svggen.ErrorConstants; +import org.apache.batik.util.ParsedURL; +import org.apache.batik.util.ParsedURLData; +import org.apache.batik.util.ParsedURLProtocolHandler; + +import de.lmu.ifi.dbs.elki.logging.Logging; + +/** + * Access images via an internal image registry. + * + * @author Erich Schubert + */ +public class ThumbnailRegistryEntry extends AbstractRegistryEntry implements URLRegistryEntry, ParsedURLProtocolHandler { + /** + * ELKI internal thumbnail protocol id. + */ + public static final String INTERNAL_PROTOCOL = "thumb"; + + /** + * ELKI internal thumbnail protocol prefix + */ + public static final String INTERNAL_PREFIX = INTERNAL_PROTOCOL + ":"; + + /** + * Mime type + */ + public static final String INTERNAL_MIME_TYPE = "internal/thumb"; + + /** + * The priority of this entry. + */ + public static final float PRIORITY = 1 * MagicNumberRegistryEntry.PRIORITY; + + /** + * The logger class. + */ + private static final Logging LOG = Logging.getLogger(ThumbnailRegistryEntry.class); + + /** + * The image cache. + */ + private static final TIntObjectMap<SoftReference<RenderedImage>> images = new TIntObjectHashMap<>(); + + /** + * Object counter + */ + private static int counter = 1; + + /** + * Constructor. + * + * Note: there will usually be two instances created. One for handling the + * image type, one for the URL handling. This is ok. + */ + public ThumbnailRegistryEntry() { + super("Internal", PRIORITY, new String[0], new String[] { INTERNAL_MIME_TYPE }); + if(LOG.isDebuggingFiner()) { + LOG.debugFiner("Registry initialized."); + } + } + + /** + * Put an image into the repository (note: the repository is only keeping a + * weak reference!) + * + * @param img Image to put + * @return Key + */ + public static int registerImage(RenderedImage img) { + synchronized(images) { + int key = counter; + counter++; + assert (images.get(key) == null); + images.put(key, new SoftReference<>(img)); + // Reorganize map, purge old entries + if(counter % 50 == 49) { + for(TIntObjectIterator<SoftReference<RenderedImage>> iter = images.iterator(); iter.hasNext();) { + iter.advance(); + if(iter.value() == null || iter.value().get() == null) { + iter.remove(); + } + } + } + if(LOG.isDebuggingFiner()) { + LOG.debugFiner("Registered image: " + key); + } + return key; + } + } + + @Override + public boolean isCompatibleURL(ParsedURL url) { + // logger.warning("isCompatible " + url.toString()); + return isCompatibleURLStatic(url); + } + + /** + * Test for a compatible URL. + * + * @param url URL + * @return Success code + */ + public static boolean isCompatibleURLStatic(ParsedURL url) { + return url.getProtocol().equals(INTERNAL_PROTOCOL); + } + + @Override + public Filter handleURL(ParsedURL url, boolean needRawData) { + Filter ret = handleURL(url); + if(ret != null) { + return ret; + } + // Image not found in registry. + return ImageTagRegistry.getBrokenLinkImage(ThumbnailRegistryEntry.this, ErrorConstants.ERR_IMAGE_DIR_DOES_NOT_EXIST, new Object[0]); + } + + /** + * Statically handle the URL access. + * + * @param url URL to access + * @return Image, or null + */ + public static Filter handleURL(ParsedURL url) { + if(LOG.isDebuggingFiner()) { + LOG.debugFiner("handleURL " + url.toString()); + } + if(!isCompatibleURLStatic(url)) { + return null; + } + int id; + try { + id = Integer.parseInt(url.getPath()); + } + catch(NumberFormatException e) { + return null; + } + SoftReference<RenderedImage> ref = images.get(id); + if(ref != null) { + RenderedImage ri = ref.get(); + if(ri == null) { + LOG.warning("Referenced image has expired from the cache!"); + } + else { + return new RedRable(GraphicsUtil.wrap(ri)); + } + } + // Image not found in registry. + return null; + } + + /** + * URL representation for internal URLs. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + class InternalParsedURLData extends ParsedURLData { + /** + * Constructor. + */ + public InternalParsedURLData(String id) { + super(); + this.protocol = INTERNAL_PROTOCOL; + this.contentType = INTERNAL_MIME_TYPE; + this.path = id; + } + + @Override + public String getContentType(String userAgent) { + return INTERNAL_MIME_TYPE; + } + + @Override + public boolean complete() { + return true; + } + + @SuppressWarnings("rawtypes") + @Override + public InputStream openStream(String userAgent, Iterator mimeTypes) throws IOException { + // Return null, since we don't want to use streams. + return null; + } + } + + @Override + public ParsedURLData parseURL(String urlStr) { + if(LOG.isDebuggingFinest()) { + LOG.debugFinest("parseURL: " + urlStr); + } + if(urlStr.startsWith(INTERNAL_PREFIX)) { + InternalParsedURLData ret = new InternalParsedURLData(urlStr.substring(INTERNAL_PREFIX.length())); + return ret; + } + return null; + } + + @Override + public ParsedURLData parseURL(ParsedURL basepurl, String urlStr) { + // Won't happen in a relative way anyway, and is not particularly + // supported (as the objects might be dropped from the cache) + return parseURL(urlStr); + } + + @Override + public String getProtocolHandled() { + return INTERNAL_PROTOCOL; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailTranscoder.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailTranscoder.java new file mode 100644 index 00000000..b0c16ef1 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailTranscoder.java @@ -0,0 +1,72 @@ +package de.lmu.ifi.dbs.elki.visualization.batikutil; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.BufferedImage; + +import org.apache.batik.transcoder.TranscoderException; +import org.apache.batik.transcoder.TranscoderOutput; +import org.apache.batik.transcoder.image.ImageTranscoder; + +/** + * Transcode images to in-memory thumbnails. + * + * @author Erich Schubert + */ +public class ThumbnailTranscoder extends ImageTranscoder { + /** + * Last image produced. + */ + private BufferedImage lastimg; + + /** + * Constructor. + */ + public ThumbnailTranscoder() { + super(); + hints.put(KEY_FORCE_TRANSPARENT_WHITE, Boolean.FALSE); + } + + @Override + public BufferedImage createImage(int width, int height) { + return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + /** + * Output will be ignored! + */ + @Override + public void writeImage(BufferedImage img, TranscoderOutput output) throws TranscoderException { + lastimg = img; + } + + /** + * Get the latest image produced. + * + * @return the last image produced + */ + public BufferedImage getLastImage() { + return lastimg; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/package-info.java new file mode 100755 index 00000000..280c5046 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Commonly used functionality useful for Apache Batik.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.batikutil;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ColorLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ColorLibrary.java new file mode 100755 index 00000000..dc668924 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ColorLibrary.java @@ -0,0 +1,93 @@ +package de.lmu.ifi.dbs.elki.visualization.colors; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +/** + * Color scheme interface + * + * @author Erich Schubert + */ +public interface ColorLibrary { + /** + * List of line colors + */ + final static String COLOR_LINE_COLORS = "line.colors"; + + /** + * Named color for the page background + */ + final static String COLOR_PAGE_BACKGROUND = "page.background"; + + /** + * Named color for a typical axis + */ + final static String COLOR_AXIS_LINE = "axis.line"; + + /** + * Named color for a typical axis tick mark + */ + final static String COLOR_AXIS_TICK = "axis.tick"; + + /** + * Named color for a typical axis tick mark + */ + final static String COLOR_AXIS_MINOR_TICK = "axis.tick.minor"; + + /** + * Named color for a typical axis label + */ + final static String COLOR_AXIS_LABEL = "axis.label"; + + /** + * Named color for the background of the key box + */ + final static String COLOR_KEY_BACKGROUND = "key.background"; + + /** + * Named color for a label in the key part + */ + final static String COLOR_KEY_LABEL = "key.label"; + + /** + * Background color for plot area + */ + final static String COLOR_PLOT_BACKGROUND = "plot.background"; + + /** + * Return the number of native colors available. These are guaranteed to be + * unique. + * + * @return number of native colors + */ + public int getNumberOfNativeColors(); + + /** + * Return the i'th color. + * + * @param index color index + * @return color in hexadecimal notation (#aabbcc) or color name ("red") as + * valid in CSS and SVG. + */ + public String getColor(int index); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ListBasedColorLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ListBasedColorLibrary.java new file mode 100644 index 00000000..60ae0a81 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ListBasedColorLibrary.java @@ -0,0 +1,73 @@ +package de.lmu.ifi.dbs.elki.visualization.colors; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + + + +/** + * Color library using the color names from a list. + * + * @author Erich Schubert + */ +public class ListBasedColorLibrary implements ColorLibrary { + /** + * Array of color names. + */ + private String[] colors; + + /** + * Color scheme name + */ + private String name; + + /** + * Constructor without a properties file name. + * + * @param colors Colors + * @param name Library name + */ + public ListBasedColorLibrary(String[] colors, String name) { + this.colors = colors; + this.name = name; + } + + @Override + public String getColor(int index) { + return colors[Math.abs(index) % colors.length]; + } + + @Override + public int getNumberOfNativeColors() { + return colors.length; + } + + /** + * Get the color scheme name. + * + * @return the name + */ + protected String getName() { + return name; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/package-info.java new file mode 100755 index 00000000..cf09bd21 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Color scheme handling for ELKI.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.colors;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClass.java new file mode 100755 index 00000000..56d15810 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClass.java @@ -0,0 +1,317 @@ +package de.lmu.ifi.dbs.elki.visualization.css; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import de.lmu.ifi.dbs.elki.utilities.pairs.Pair; + +/** + * Class representing a single CSS class. + * + * @author Erich Schubert + */ +public class CSSClass { + /** + * CSS class name + */ + private String name; + + /** + * Actual CSS statements + */ + private Collection<Pair<String, String>> statements; + + /** + * Owner. + */ + private WeakReference<Object> owner; + + /** + * Full constructor + * + * @param owner Class owner (to detect conflicts) + * @param name Class name + * @param statements Collection of CSS statements + */ + public CSSClass(Object owner, String name, Collection<Pair<String, String>> statements) { + this.owner = new WeakReference<>(owner); + this.name = name; + this.statements = statements; + if (!checkName(name)) { + throw new InvalidCSS("Given name is not a valid CSS class name."); + } + if (this.statements != null) { + if (!checkCSSStatements(this.statements)) { + throw new InvalidCSS("Invalid statement in CSS class "+name); + } + } else { + // if needed, use an array list. + this.statements = new ArrayList<>(); + } + } + + /** + * Simplified constructor, empty statements list. + * + * @param owner Class owner. + * @param name Class name. + */ + public CSSClass(Object owner, String name) { + this(owner, name, (Collection<Pair<String,String>>) null); + } + + /** + * Cloning constructor + * + * @param owner Class owner. + * @param name Class name. + * @param other Class to clone + */ + public CSSClass(Object owner, String name, CSSClass other) { + this(owner, name, new ArrayList<>(other.statements)); + } + + /** + * Verify that the name is an admissible CSS class name. + * + * TODO: implement. + * + * @param name name to use + * @return true if valid CSS class name + */ + public static boolean checkName(String name) { + // TODO: implement a sanity check - regexp? + return (name != null); + } + + /** + * Return a sanitized version of the given string. + * + * TODO: implement extensive checks. + * + * @param name name to sanitize + * @return Sanitized version. + */ + public static String sanitizeName(String name) { + // TODO: implement a sanitization - regexp? + return name; + } + + /** + * Validate a single CSS statement. + * + * TODO: implement extensive checks. + * + * @param key Key + * @param value Value + * @return true if valid statement. + */ + public static boolean checkCSSStatement(String key, String value) { + // TODO: implement more extensive checks! + return (key != null) && (value != null); + } + + /** + * Validate a set of CSS statements. + * + * TODO: checks are currently not very extensive. + * + * @param statements Statements to check + * @return true if valid + */ + public static boolean checkCSSStatements(Collection<Pair<String,String>> statements) { + for (Pair<String, String> pair : statements) { + if (!checkCSSStatement(pair.getFirst(), pair.getSecond())) { + return false; + } + } + return true; + } + + /** + * Get the class name. + * + * @return class name. + */ + public String getName() { + return this.name; + } + + /** + * Set the class name. + * + * @param name new class name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get class owner. + * + * @return class owner. + */ + public Object getOwner() { + return this.owner.get(); + } + + /** + * Get the current value of a particular CSS statement. + * + * @param key statement key. + * @return current value or null. + */ + public String getStatement(String key) { + for (Pair<String, String> pair : statements) { + if (pair.getFirst().equals(key)) { + return pair.getSecond(); + } + } + return null; + } + + /** + * Get read-only collection access to all statements. + * + * @return Collection + */ + public Collection<Pair<String, String>> getStatements() { + return Collections.unmodifiableCollection(statements); + } + + /** + * Set a CSS statement. + * + * @param key Statement key. + * @param value Value or null (to unset) + */ + public void setStatement(String key, String value) { + if (value != null) { + if (!checkCSSStatement(key, value)) { + throw new InvalidCSS("Invalid CSS statement."); + } + } + for (Pair<String, String> pair : statements) { + if (pair.getFirst().equals(key)) { + if (value != null) { + pair.setSecond(value); + } else { + statements.remove(pair); + } + return; + } + } + if (value != null) { + statements.add(new Pair<>(key, value)); + } + } + + /** + * Set a CSS statement. + * + * @param key Statement key. + * @param value Value + */ + public void setStatement(String key, int value) { + setStatement(key, Integer.toString(value)); + } + + /** + * Set a CSS statement. + * + * @param key Statement key. + * @param value Value + */ + public void setStatement(String key, double value) { + setStatement(key, Double.toString(value)); + } + + /** + * Remove a CSS statement. + * + * @param key Statement key. + */ + public void removeStatement(String key) { + setStatement(key, null); + } + + /** + * Append CSS definition to a stream + * + * @param buf String buffer to append to. + */ + public void appendCSSDefinition(StringBuilder buf) { + buf.append("\n."); + buf.append(name); + buf.append('{'); + for (Pair<String, String> pair : statements) { + buf.append(pair.getFirst()); + buf.append(':'); + buf.append(pair.getSecond()); + buf.append(";\n"); + } + buf.append("}\n"); + } + + /** + * Exception class thrown when encountering invalid CSS. + * + * @apiviz.exclude + */ + public class InvalidCSS extends RuntimeException { + /** + * Constructor. See {@link RuntimeException}. + * + * @param msg Error message. + */ + public InvalidCSS(String msg) { + super(msg); + } + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 3130536799704124363L; + } + + /** + * Render CSS class to inline formatting + * + * @return string rendition of CSS for inline use + */ + public String inlineCSS() { + StringBuilder buf = new StringBuilder(); + for (Pair<String, String> pair : statements) { + buf.append(pair.getFirst()); + buf.append(':'); + buf.append(pair.getSecond()); + buf.append(';'); + } + return buf.toString(); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClassManager.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClassManager.java new file mode 100755 index 00000000..003f6f74 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClassManager.java @@ -0,0 +1,216 @@ +package de.lmu.ifi.dbs.elki.visualization.css; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Collection; +import java.util.HashMap; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Text; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Manager class to track CSS classes used in a particular SVG document. + * + * @author Erich Schubert + * + * @apiviz.has de.lmu.ifi.dbs.elki.visualization.css.CSSClass + */ +public class CSSClassManager { + /** + * Store the contained CSS classes. + */ + private HashMap<String, CSSClass> store = new HashMap<>(); + + /** + * Add a single class to the map. + * + * @param clss new CSS class + * @return existing (old) class + * @throws CSSNamingConflict when a class of the same name but different owner object exists. + */ + public CSSClass addClass(CSSClass clss) throws CSSNamingConflict { + CSSClass existing = store.get(clss.getName()); + if (existing != null && existing.getOwner() != null && existing.getOwner() != clss.getOwner()) { + throw new CSSNamingConflict("CSS class naming conflict between "+clss.getOwner().toString()+" and "+existing.getOwner().toString()); + } + return store.put(clss.getName(), clss); + } + + /** + * Remove a single CSS class from the map. + * Note that classes are removed by reference, not by name! + * + * @param clss Class to remove + */ + public synchronized void removeClass(CSSClass clss) { + CSSClass existing = store.get(clss.getName()); + if (existing == clss) { + store.remove(existing.getName()); + } + } + + /** + * Retrieve a single class by name and owner + * + * @param name Class name + * @param owner Class owner + * @return existing (old) class + * @throws CSSNamingConflict if an owner was specified and doesn't match + */ + public CSSClass getClass(String name, Object owner) throws CSSNamingConflict { + CSSClass existing = store.get(name); + // Not found. + if (existing == null) { + return null; + } + // Different owner + if (owner != null && existing.getOwner() != owner) { + throw new CSSNamingConflict("CSS class naming conflict between "+owner.toString()+" and "+existing.getOwner().toString()); + } + return existing; + } + + /** + * Retrieve a single class by name only + * + * @param name CSS class name + * @return existing (old) class + */ + public CSSClass getClass(String name) { + return store.get(name); + } + + /** + * Check if a name is already used in the classes. + * + * @param name CSS class name + * @return true if the class name is already used. + */ + public boolean contains(String name) { + return store.containsKey(name); + } + + /** + * Serialize managed CSS classes to rule file. + * + * @param buf String buffer + */ + public void serialize(StringBuilder buf) { + for (CSSClass clss : store.values()) { + clss.appendCSSDefinition(buf); + } + } + + /** + * Get all CSS classes in this manager. + * + * @return CSS classes. + */ + public Collection<CSSClass> getClasses() { + return store.values(); + } + + /** + * Check whether or not CSS classes of two plots can be merged + * + * @param other Other class + * @return true if able to merge + */ + public boolean testMergeable(CSSClassManager other) { + for (CSSClass clss : other.getClasses()) { + CSSClass existing = store.get(clss.getName()); + // Check for a naming conflict. + if (existing != null && existing.getOwner() != null && clss.getOwner() != null && existing.getOwner() != clss.getOwner()) { + return false; + } + } + return true; + } + + /** + * Merge CSS classes, for example to merge two plots. + * + * @param other Other class to merge with + * @return success code + * @throws CSSNamingConflict If there is a naming conflict. + */ + public boolean mergeCSSFrom(CSSClassManager other) throws CSSNamingConflict { + for (CSSClass clss : other.getClasses()) { + this.addClass(clss); + } + return true; + } + + /** + * Class to signal a CSS naming conflict. + * + * @apiviz.exclude + */ + public class CSSNamingConflict extends Exception { + /** + * Serial version UID + */ + private static final long serialVersionUID = 4163822727195636747L; + + /** + * Exception to signal a CSS naming conflict. + * + * @param msg Exception message + */ + public CSSNamingConflict(String msg) { + super(msg); + } + } + + /** + * Update the text contents of an existing style element. + * + * @param document Document element (factory) + * @param style Style element + */ + public void updateStyleElement(Document document, Element style) { + StringBuilder buf = new StringBuilder(); + serialize(buf); + Text cont = document.createTextNode(buf.toString()); + while (style.hasChildNodes()) { + style.removeChild(style.getFirstChild()); + } + style.appendChild(cont); + } + + /** + * Make a (filled) CSS style element for the given document. + * + * @param document Document + * @return Style element + */ + public Element makeStyleElement(Document document) { + Element style = SVGUtil.makeStyleElement(document); + updateStyleElement(document, style); + return style; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/package-info.java new file mode 100755 index 00000000..1c936cf1 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Managing CSS styles / classes.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.css;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultVisualizer.java new file mode 100644 index 00000000..64ddfaa3 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultVisualizer.java @@ -0,0 +1,190 @@ +package de.lmu.ifi.dbs.elki.visualization.gui; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import javax.swing.JFrame; + +import de.lmu.ifi.dbs.elki.gui.GUIUtil; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultHandler; +import de.lmu.ifi.dbs.elki.result.ResultHierarchy; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.utilities.Alias; +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.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.StringParameter; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.VisualizerParameterizer; + +/** + * Handler to process and visualize a Result. + * + * @author Erich Schubert + * @author Remigius Wojdanowski + * + * @apiviz.composedOf VisualizerParameterizer + * @apiviz.uses ResultWindow oneway + */ +@Alias({ "visualizer", "vis", "ResultVisualizer" }) +public class ResultVisualizer implements ResultHandler { + /** + * Get a logger for this class. + */ + private static final Logging LOG = Logging.getLogger(ResultVisualizer.class); + + /** + * Stores the set title. + */ + String title; + + /** + * Default title + */ + protected static final String DEFAULT_TITLE = "ELKI Result Visualization"; + + /** + * Visualization manager. + */ + VisualizerParameterizer manager; + + /** + * Single view mode + */ + boolean single; + + /** + * Current result window. + */ + ResultWindow window; + + /** + * Constructor. + * + * @param title Window title + * @param manager Parameterization manager for visualizers + * @param single Flag to indicat single-view mode. + */ + public ResultVisualizer(String title, VisualizerParameterizer manager, boolean single) { + super(); + this.title = title; + this.manager = manager; + this.single = single; + } + + @Override + public void processNewResult(final ResultHierarchy hier, final Result result) { + if(window == null) { + if(title == null) { + title = VisualizerParameterizer.getTitle(ResultUtil.findDatabase(hier), result); + if(title == null) { + title = DEFAULT_TITLE; + } + } + + GUIUtil.setLookAndFeel(); + VisualizerContext context = manager.newContext(hier, result); + window = new ResultWindow(title, context, single); + } + + javax.swing.SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try { + window.setVisible(true); + window.setExtendedState(window.getExtendedState() | JFrame.MAXIMIZED_BOTH); + } + catch(Throwable e) { + LOG.exception("Error in starting visualizer window.", e); + } + } + }); + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Parameter to specify the window title + * <p> + * Key: {@code -vis.window.title} + * </p> + * <p> + * Default value: "ELKI Result Visualization" + * </p> + */ + public static final OptionID WINDOW_TITLE_ID = new OptionID("vis.window.title", "Title to use for visualization window."); + + /** + * Flag to set single display + * + * <p> + * Key: -vis.single + * </p> + */ + public static final OptionID SINGLE_ID = new OptionID("vis.window.single", "Embed visualizers in a single window, not using thumbnails and detail views."); + + /** + * Stores the set title. + */ + String title; + + /** + * Visualization manager. + */ + VisualizerParameterizer manager; + + /** + * Single view mode. + */ + boolean single = false; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + StringParameter titleP = new StringParameter(WINDOW_TITLE_ID); + titleP.setOptional(true); + if(config.grab(titleP)) { + title = titleP.getValue(); + } + Flag singleF = new Flag(SINGLE_ID); + if(config.grab(singleF)) { + single = singleF.isTrue(); + } + manager = config.tryInstantiate(VisualizerParameterizer.class); + } + + @Override + protected ResultVisualizer makeInstance() { + return new ResultVisualizer(title, manager, single); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultWindow.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultWindow.java new file mode 100644 index 00000000..f82a21a0 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultWindow.java @@ -0,0 +1,641 @@ +package de.lmu.ifi.dbs.elki.visualization.gui; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.Collection; + +import javax.swing.ImageIcon; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JRadioButtonMenuItem; +import javax.swing.SwingUtilities; + +import org.apache.batik.swing.svg.GVTTreeBuilderAdapter; +import org.apache.batik.swing.svg.GVTTreeBuilderEvent; + +import de.lmu.ifi.dbs.elki.KDDTask; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultHierarchy; +import de.lmu.ifi.dbs.elki.result.ResultListener; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizationListener; +import de.lmu.ifi.dbs.elki.visualization.VisualizationMenuAction; +import de.lmu.ifi.dbs.elki.visualization.VisualizationMenuToggle; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.JSVGSynchronizedCanvas; +import de.lmu.ifi.dbs.elki.visualization.batikutil.LazyCanvasResizer; +import de.lmu.ifi.dbs.elki.visualization.gui.detail.DetailView; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.DetailViewSelectedEvent; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.OverviewPlot; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.savedialog.SVGSaveDialog; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * Swing window to manage a particular result visualization. + * + * Yes, this is very basic and ad-hoc. Feel free to contribute something more + * advanced to ELKI! + * + * @author Erich Schubert + * @author Remigius Wojdanowski + * + * @apiviz.composedOf JSVGSynchronizedCanvas + * @apiviz.composedOf OverviewPlot + * @apiviz.composedOf SelectionTableWindow + * @apiviz.composedOf SVGSaveDialog + * @apiviz.composedOf LazyCanvasResizer + * @apiviz.has VisualizerContext + * @apiviz.uses DetailView oneway + * @apiviz.uses DetailViewSelectedEvent oneway - - reacts to + */ +public class ResultWindow extends JFrame implements ResultListener, VisualizationListener { + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + /** + * Get a logger for this class. + */ + private static final Logging LOG = Logging.getLogger(ResultWindow.class); + + /** + * Dynamic menu. + * + * @apiviz.exclude + */ + public class DynamicMenu { + /** + * Menubar component + */ + private JMenuBar menubar; + + /** + * File menu. + */ + private JMenu filemenu; + + /** + * The "Overview" button, which goes to the overview view. + */ + private JMenuItem overviewItem; + + /** + * The "Quit" button, to close the application. + */ + private JMenuItem quitItem; + + /** + * The "Export" button, to save the image + */ + private JMenuItem exportItem; + + /** + * The "tabular edit" item. + */ + private JMenuItem editItem; + + /** + * The "Visualizers" button, to enable/disable visualizers + */ + private JMenu visualizersMenu; + + /** + * Simplify the menu. + */ + protected boolean simplify = true; + + /** + * Constructor. + */ + public DynamicMenu() { + menubar = new JMenuBar(); + filemenu = new JMenu("File"); + filemenu.setMnemonic(KeyEvent.VK_F); + + // setup buttons + if(!single) { + overviewItem = new JMenuItem("Open Overview"); + overviewItem.setMnemonic(KeyEvent.VK_O); + overviewItem.setEnabled(false); + overviewItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + showOverview(); + } + }); + filemenu.add(overviewItem); + } + + exportItem = new JMenuItem("Export Plot"); + exportItem.setMnemonic(KeyEvent.VK_E); + exportItem.setEnabled(false); + exportItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + saveCurrentPlot(); + } + }); + filemenu.add(exportItem); + + editItem = new JMenuItem("Table View/Edit"); + editItem.setMnemonic(KeyEvent.VK_T); + editItem.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent ae) { + showTableView(); + } + }); + // FIXME: re-add when it is working again. + // filemenu.add(editItem); + + quitItem = new JMenuItem("Quit"); + quitItem.setMnemonic(KeyEvent.VK_Q); + quitItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + close(); + } + }); + + filemenu.add(quitItem); + menubar.add(filemenu); + + visualizersMenu = new JMenu("Visualizers"); + visualizersMenu.setMnemonic(KeyEvent.VK_V); + menubar.add(visualizersMenu); + } + + /** + * Update the visualizer menus. + */ + protected synchronized void updateVisualizerMenus() { + Projection proj = null; + if(svgCanvas.getPlot() instanceof DetailView) { + PlotItem item = ((DetailView) svgCanvas.getPlot()).getPlotItem(); + proj = item.proj; + } + menubar.removeAll(); + menubar.add(filemenu); + ResultHierarchy hier = context.getHierarchy(); + Hierarchy<Object> vistree = context.getVisHierarchy(); + Result start = context.getBaseResult(); + ArrayList<JMenuItem> items = new ArrayList<>(); + if(start == null) { + for(Hierarchy.Iter<Result> iter = hier.iterAll(); iter.valid(); iter.advance()) { + if(hier.numParents(iter.get()) == 0) { + recursiveBuildMenu(items, iter.get(), hier, vistree, proj); + } + } + } + else { + for(Hierarchy.Iter<Result> iter = hier.iterChildren(start); iter.valid(); iter.advance()) { + recursiveBuildMenu(items, iter.get(), hier, vistree, proj); + } + } + // Add all items. + for(JMenuItem item : items) { + menubar.add(item); + } + menubar.revalidate(); + menubar.repaint(); + } + + private void recursiveBuildMenu(Collection<JMenuItem> items, Object r, ResultHierarchy hier, Hierarchy<Object> vistree, Projection proj) { + // Make a submenu for this element + final String nam; + if(r instanceof Result) { + nam = ((Result) r).getLongName(); + } + else if(r instanceof VisualizationItem) { + nam = ((VisualizationItem) r).getMenuName(); + } + else { + return; + } + ArrayList<JMenuItem> subitems = new ArrayList<>(); + // Add menus for any child results + if(r instanceof Result) { + for(Hierarchy.Iter<Result> iter = hier.iterChildren((Result) r); iter.valid(); iter.advance()) { + recursiveBuildMenu(subitems, iter.get(), hier, vistree, proj); + } + } + // Add visualizers: + for(Hierarchy.Iter<Object> iter = vistree.iterChildren(r); iter.valid(); iter.advance()) { + recursiveBuildMenu(subitems, iter.get(), hier, vistree, proj); + } + + // Item for the visualizer + JMenuItem item = null; + if(proj == null) { + item = makeMenuItemForVisualizer(r); + } + else { + // Only include items that belong to different projections: + for(Hierarchy.Iter<Object> iter = vistree.iterAncestorsSelf(r); iter.valid(); iter.advance()) { + if(iter.get() == proj.getProjector()) { + item = makeMenuItemForVisualizer(r); + break; + } + } + } + final int numchild = subitems.size(); + if(numchild == 0) { + if(item != null) { + items.add(item); + } + return; + } + if(simplify && numchild == 1) { + JMenuItem a = subitems.get(0); + if(a instanceof JMenu) { + if(nam != null) { + a.setText(nam + " " + a.getText()); + } + items.add(a); + return; + } + } + JMenu submenu = new JMenu((nam != null) ? nam : "unnamed"); + if(item != null) { + submenu.add(item); + } + for(JMenuItem subitem : subitems) { + submenu.add(subitem); + } + items.add(submenu); + } + + private JMenuItem makeMenuItemForVisualizer(Object r) { + if(r instanceof VisualizationMenuAction) { + final VisualizationMenuAction action = (VisualizationMenuAction) r; + JMenuItem visItem = new JMenuItem(action.getMenuName()); + visItem.setEnabled(action.enabled()); + visItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + action.activate(); + } + }); + return visItem; + } + if(r instanceof VisualizationMenuToggle) { + final VisualizationMenuToggle toggle = (VisualizationMenuToggle) r; + final JCheckBoxMenuItem visItem = new JCheckBoxMenuItem(toggle.getMenuName(), toggle.active()); + visItem.setEnabled(toggle.enabled()); + visItem.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + toggle.toggle(); + } + }); + return visItem; + } + if(!(r instanceof VisualizationTask)) { + return null; + } + final VisualizationTask v = (VisualizationTask) r; + JMenuItem item; + + // Currently enabled? + final String name = v.getMenuName(); + boolean enabled = v.visible; + boolean istool = v.tool; + if(!istool) { + final JCheckBoxMenuItem visItem = new JCheckBoxMenuItem(name, enabled); + visItem.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + VisualizationTree.setVisible(context, v, visItem.getState()); + } + }); + item = visItem; + } + else { + final JRadioButtonMenuItem visItem = new JRadioButtonMenuItem(name, enabled); + visItem.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + VisualizationTree.setVisible(context, v, visItem.isSelected()); + } + }); + item = visItem; + } + return item; + } + + /** + * Get the menu bar component. + * + * @return Menu bar component + */ + public JMenuBar getMenuBar() { + return menubar; + } + + /** + * Enable / disable the overview menu. + * + * @param b Flag + */ + public void enableOverview(boolean b) { + if(overviewItem != null) { + overviewItem.setEnabled(b); + } + } + + /** + * Enable / disable the export menu. + * + * @param b Flag + */ + public void enableExport(boolean b) { + exportItem.setEnabled(b); + } + } + + private DynamicMenu menubar; + + /** + * The SVG canvas. + */ + private JSVGSynchronizedCanvas svgCanvas; + + /** + * The overview plot. + */ + private OverviewPlot overview; + + /** + * Visualizer context + */ + protected VisualizerContext context; + + /** + * Currently selected subplot. + */ + private DetailView currentSubplot = null; + + /** + * Single view mode. No overview / detail view split + */ + private boolean single = false; + + /** + * Constructor. + * + * @param title Window title + * @param context Visualizer context + * @param single Single visualization mode + */ + public ResultWindow(String title, VisualizerContext context, boolean single) { + super(title); + this.context = context; + this.single = single; + + // close handler + this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + // ELKI icon + try { + setIconImage(new ImageIcon(KDDTask.class.getResource("elki-icon.png")).getImage()); + } + catch(Exception e) { + // Ignore - icon not found is not fatal. + } + + // Create a panel and add the button, status label and the SVG canvas. + final JPanel panel = new JPanel(new BorderLayout()); + + menubar = new DynamicMenu(); + panel.add("North", menubar.getMenuBar()); + + svgCanvas = new JSVGSynchronizedCanvas(); + panel.add("Center", svgCanvas); + + this.getContentPane().add(panel); + + overview = new OverviewPlot(context, single); + // when a subplot is clicked, show the selected subplot. + overview.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if(e instanceof DetailViewSelectedEvent) { + showSubplot((DetailViewSelectedEvent) e); + } + if(OverviewPlot.OVERVIEW_REFRESHING == e.getActionCommand()) { + if(currentSubplot == null) { + showPlot(null); + } + } + if(OverviewPlot.OVERVIEW_REFRESHED == e.getActionCommand()) { + if(currentSubplot == null) { + showOverview(); + } + } + } + }); + + // handle screen size + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + overview.screenwidth = dim.width; + overview.screenheight = dim.height; + + // Maximize. + this.setSize(dim.width - 50, dim.height - 50); + this.setExtendedState(JFrame.MAXIMIZED_BOTH); + + // resize listener + final LazyCanvasResizer listener = new LazyCanvasResizer(this, 0.1) { + @Override + public void executeResize(double newratio) { + ResultWindow.this.handleResize(newratio); + } + }; + this.addComponentListener(listener); + svgCanvas.addGVTTreeBuilderListener(new GVTTreeBuilderAdapter() { + @Override + public void gvtBuildCompleted(GVTTreeBuilderEvent arg0) { + // Supposedly in Swing thread. + menubar.updateVisualizerMenus(); + } + }); + + context.addResultListener(this); + context.addVisualizationListener(this); + overview.initialize(listener.getCurrentRatio()); + } + + @Override + public void dispose() { + context.removeResultListener(this); + context.removeVisualizationListener(this); + svgCanvas.setPlot(null); + overview.destroy(); + if(currentSubplot != null) { + currentSubplot.dispose(); + currentSubplot = null; + } + super.dispose(); + } + + /** + * Close the visualizer window. + */ + protected void close() { + this.setVisible(false); + this.dispose(); + } + + /** + * Navigate to the overview plot. + */ + public void showOverview() { + if(currentSubplot != null) { + currentSubplot.destroy(); + } + currentSubplot = null; + showPlot(overview.getPlot()); + } + + /** + * Navigate to a subplot. + * + * @param e + */ + protected void showSubplot(DetailViewSelectedEvent e) { + if(!single) { + currentSubplot = e.makeDetailView(); + showPlot(currentSubplot); + } + } + + /** + * Navigate to a particular plot. + * + * @param plot Plot to show. + */ + private void showPlot(final SVGPlot plot) { + if(svgCanvas.getPlot() instanceof DetailView) { + ((DetailView) svgCanvas.getPlot()).destroy(); + } + svgCanvas.setPlot(plot); + menubar.enableOverview(plot != overview.getPlot()); + menubar.enableExport(plot != null); + updateVisualizerMenus(); + } + + /** + * Save/export the current plot. + */ + protected void saveCurrentPlot() { + final SVGPlot currentPlot = svgCanvas.getPlot(); + if(currentPlot == null) { + LOG.warning("saveCurrentPlot() called without a visible plot!"); + return; + } + SVGSaveDialog.showSaveDialog(currentPlot, 512, 512); + } + + /** + * Show a tabular view + */ + protected void showTableView() { + (new SelectionTableWindow(context)).setVisible(true); + } + + /** + * Refresh the overview + */ + protected void update() { + updateVisualizerMenus(); + if(currentSubplot != null) { + showPlot(currentSubplot); + } + overview.lazyRefresh(); + } + + /** + * Handle a resize event. + * + * @param newratio New window size ratio. + */ + protected void handleResize(double newratio) { + if(currentSubplot == null) { + ResultWindow.this.overview.setRatio(newratio); + } + } + + @Override + public void resultAdded(Result child, Result parent) { + updateVisualizerMenus(); + } + + @Override + public void resultChanged(Result current) { + updateVisualizerMenus(); + } + + @Override + public void resultRemoved(Result child, Result parent) { + updateVisualizerMenus(); + } + + @Override + public void visualizationChanged(VisualizationItem item) { + updateVisualizerMenus(); + } + + /** + * Update visualizer menus, but only from Swing thread. + */ + private void updateVisualizerMenus() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + menubar.updateVisualizerMenus(); + } + }); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SelectionTableWindow.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SelectionTableWindow.java new file mode 100644 index 00000000..5e9af1ed --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SelectionTableWindow.java @@ -0,0 +1,376 @@ +package de.lmu.ifi.dbs.elki.visualization.gui; +/* +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 <http://www.gnu.org/licenses/>. +*/ + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; + +import de.lmu.ifi.dbs.elki.KDDTask; +import de.lmu.ifi.dbs.elki.data.ClassLabel; +import de.lmu.ifi.dbs.elki.data.SimpleClassLabel; +import de.lmu.ifi.dbs.elki.database.Database; +import de.lmu.ifi.dbs.elki.database.UpdatableDatabase; +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.ids.ArrayModifiableDBIDs; +import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs; +import de.lmu.ifi.dbs.elki.database.relation.ModifiableRelation; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultListener; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.SelectionResult; +import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; + +/** + * Visualizes selected Objects in a JTable, objects can be selected, changed and + * deleted + * + * @author Heidi Kolb + * @author Erich Schubert + */ +// FIXME: INCOMPLETE TRANSITION TO MULTI-REPRESENTED DATA +public class SelectionTableWindow extends JFrame implements DataStoreListener, ResultListener { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Selected data objects"; + + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + /** + * The JTable + */ + private JTable table; + + /** + * Button to close the window + */ + private JButton closeButton; + + /** + * Button to delete the selected objects + */ + private JButton deleteButton; + + /** + * The table model + */ + private DatabaseTableModel dotTableModel = new DatabaseTableModel(); + + /** + * The logger + */ + private static final Logging LOG = Logging.getLogger(SelectionTableWindow.class); + + /** + * The DBIDs to display + */ + ArrayModifiableDBIDs dbids; + + /** + * The database we use + */ + UpdatableDatabase database; + + /** + * Class label representation + */ + ModifiableRelation<ClassLabel> crep; + + /** + * Object label representation + */ + ModifiableRelation<String> orep; + + /** + * Our context + */ + final protected VisualizerContext context; + + /** + * The actual visualization instance, for a single projection + * + * @param context The Context + */ + public SelectionTableWindow(VisualizerContext context) { + super(NAME); + // ELKI icon + try { + setIconImage(new ImageIcon(KDDTask.class.getResource("elki-icon.png")).getImage()); + } + catch(Exception e) { + // Ignore - icon not found is not fatal. + } + + this.context = context; + this.database = (UpdatableDatabase) ResultUtil.findDatabase(context.getHierarchy()); + // FIXME: re-add labels + this.crep = null; //database.getClassLabelQuery(); + this.orep = null; //database.getObjectLabelQuery(); + updateFromSelection(); + + JPanel panel = new JPanel(new BorderLayout()); + + table = new JTable(dotTableModel); + + // table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + JScrollPane pane = new JScrollPane(table); + panel.add(pane, BorderLayout.CENTER); + + JPanel buttons = new JPanel(); + panel.add(buttons, BorderLayout.SOUTH); + + closeButton = new JButton("close"); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + dispose(); + } + }); + deleteButton = new JButton("delete"); + deleteButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + handleDelete(); + } + }); + buttons.add(closeButton); + buttons.add(deleteButton); + + setSize(500, 500); + add(panel); + setVisible(true); + setResizable(true); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + // Listen for Selection and Database changes. + context.addResultListener(this); + context.addDataStoreListener(this); + } + + @Override + public void dispose() { + context.removeDataStoreListener(this); + context.removeResultListener(this); + super.dispose(); + } + + /** + * Update our selection + */ + protected void updateFromSelection() { + DBIDSelection sel = context.getSelection(); + if(sel != null) { + this.dbids = DBIDUtil.newArray(sel.getSelectedIds()); + this.dbids.sort(); + } + else { + this.dbids = DBIDUtil.newArray(); + } + } + + /** + * Handle delete. <br> + * Delete the marked objects in the database. + */ + protected void handleDelete() { + ModifiableDBIDs todel = DBIDUtil.newHashSet(); + ModifiableDBIDs remain = DBIDUtil.newHashSet(dbids); + DBIDArrayIter it = dbids.iter(); + for(int row : table.getSelectedRows()) { + it.seek(row); + todel.add(it); + remain.remove(it); + } + // Unselect first ... + context.setSelection(new DBIDSelection(remain)); + // Now delete them. + for(DBIDIter iter = todel.iter(); iter.valid(); iter.advance()) { + database.delete(iter); + } + } + + /** + * View onto the database + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + class DatabaseTableModel extends AbstractTableModel { + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + @Override + public int getColumnCount() { + return 3; //RelationUtil.dimensionality(database) + 3; + } + + @Override + public int getRowCount() { + return dbids.size(); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + DBIDRef id = dbids.iter().seek(rowIndex); + if(columnIndex == 0) { + return DBIDUtil.toString(id); + } + if(columnIndex == 1) { + return orep.get(id); + } + if(columnIndex == 2) { + return crep.get(id); + } + /*NV obj = database.get(id); + if(obj == null) { + return null; + } + return obj.getValue(columnIndex - 3 + 1);*/ + return null; + } + + @Override + public String getColumnName(int column) { + if(column == 0) { + return "DBID"; + } + if(column == 1) { + return "Object label"; + } + if(column == 2) { + return "Class label"; + } + return "Dim " + (column - 3 + 1); + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + if(columnIndex == 0) { + return false; + } + return true; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if(columnIndex == 0) { + LOG.warning("Tried to edit DBID, this is not allowed."); + return; + } + final DBIDRef id = dbids.iter().seek(rowIndex); + if(columnIndex == 1 && aValue instanceof String) { + orep.insert(id, (String) aValue); + } + if(columnIndex == 2 && aValue instanceof String) { + // FIXME: better class label handling! + SimpleClassLabel lbl = new SimpleClassLabel((String) aValue); + crep.insert(id, lbl); + } + if(!(aValue instanceof String)) { + LOG.warning("Was expecting a String value from the input element, got: " + aValue.getClass()); + return; + } + throw new AbortException("FIXME: INCOMPLETE TRANSITION"); + /* NV obj = database.get(id); + if(obj == null) { + logger.warning("Tried to edit removed object?"); + return; + } + final int dimensionality = RelationUtil.dimensionality(database); + double[] vals = new double[dimensionality]; + for(int d = 0; d < dimensionality; d++) { + if(d == columnIndex - 3) { + vals[d] = FormatUtil.parseDouble((String) aValue); + } + else { + vals[d] = obj.doubleValue(d + 1); + } + } + NV newobj = obj.newInstance(vals); + newobj.setID(id); + final Representation<DatabaseObjectMetadata> mrep = database.getMetadataQuery(); + DatabaseObjectMetadata meta = mrep.get(id); + try { + database.delete(id); + database.insert(new Pair<NV, DatabaseObjectMetadata>(newobj, meta)); + } + catch(UnableToComplyException e) { + de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e); + } */ + // TODO: refresh wrt. range selection! + } + } + + @Override + public void contentChanged(DataStoreEvent e) { + if (e.getInserts().isEmpty() && e.getRemovals().isEmpty() && !e.getUpdates().isEmpty()) { + // Updates only. + dotTableModel.fireTableDataChanged(); + } + else { + dotTableModel.fireTableStructureChanged(); + } + } + + @Override + public void resultAdded(Result child, Result parent) { + // TODO Auto-generated method stub + } + + @Override + public void resultRemoved(Result child, Result parent) { + // TODO Auto-generated method stub + } + + @Override + public void resultChanged(Result current) { + if (current instanceof SelectionResult || current instanceof Database) { + updateFromSelection(); + dotTableModel.fireTableStructureChanged(); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SimpleSVGViewer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SimpleSVGViewer.java new file mode 100644 index 00000000..cdd7758c --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SimpleSVGViewer.java @@ -0,0 +1,149 @@ +package de.lmu.ifi.dbs.elki.visualization.gui; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.HeadlessException; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +import javax.swing.JFrame; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.UIManager; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.visualization.batikutil.JSVGSynchronizedCanvas; +import de.lmu.ifi.dbs.elki.visualization.savedialog.SVGSaveDialog; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * A minimalistic SVG viewer with export dialog. + * + * @author Erich Schubert + */ +public class SimpleSVGViewer extends JFrame { + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + /** + * The main canvas. + */ + private JSVGSynchronizedCanvas svgCanvas; + + /** + * Constructor. + * + * @throws HeadlessException + */ + public SimpleSVGViewer() throws HeadlessException { + super(); + // Prefer system look&feel + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + catch(Exception e) { + // ignore + } + + // close handler + this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + // Maximize. + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + setSize(dim.width - 50, dim.height - 50); + + // setup buttons + JMenuItem exportItem = new JMenuItem("Export"); + exportItem.setMnemonic(KeyEvent.VK_E); + exportItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent ae) { + saveCurrentPlot(); + } + }); + + JMenuItem quitItem = new JMenuItem("Quit"); + quitItem.setMnemonic(KeyEvent.VK_Q); + quitItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + close(); + } + }); + + // Create a panel and add the button, status label and the SVG canvas. + final JPanel panel = new JPanel(new BorderLayout()); + + JMenuBar menubar = new JMenuBar(); + menubar.add(exportItem); + menubar.add(quitItem); + + panel.add("North", menubar); + + svgCanvas = new JSVGSynchronizedCanvas(); + panel.add("Center", svgCanvas); + + this.getContentPane().add(panel); + + setExtendedState(JFrame.MAXIMIZED_BOTH); + this.setVisible(true); + } + + /** + * Close the visualizer window. + */ + public void close() { + this.setVisible(false); + this.dispose(); + } + + /** + * Save/export the current plot. + */ + public void saveCurrentPlot() { + // TODO: exclude "do not export" layers! + final SVGPlot currentPlot = svgCanvas.getPlot(); + if(currentPlot != null) { + SVGSaveDialog.showSaveDialog(currentPlot, 512, 512); + } + else { + LoggingUtil.warning("saveCurrentPlot() called without a visible plot!"); + } + } + + /** + * Set the plot to show + * + * @param plot Plot + */ + public void setPlot(SVGPlot plot) { + svgCanvas.setPlot(plot); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/VisualizationPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/VisualizationPlot.java new file mode 100644 index 00000000..7360a0da --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/VisualizationPlot.java @@ -0,0 +1,81 @@ +package de.lmu.ifi.dbs.elki.visualization.gui; +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.concurrent.ConcurrentLinkedDeque; + +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * SVG plot that allows visualization to schedule updates. + * + * @author Erich Schubert + */ +public class VisualizationPlot extends SVGPlot { + /** + * Pending redraw request in Batik. + */ + protected Runnable pendingRedraw = null; + + /** + * Update queue. + */ + protected ConcurrentLinkedDeque<Visualization> updateQueue = new ConcurrentLinkedDeque<>(); + + /** + * Trigger a redraw, but avoid excessive redraws. + */ + protected final void synchronizedRedraw() { + Runnable pr = new Runnable() { + @Override + public void run() { + if(VisualizationPlot.this.pendingRedraw == this) { + VisualizationPlot.this.pendingRedraw = null; + VisualizationPlot.this.redraw(); + } + } + }; + pendingRedraw = pr; + scheduleUpdate(pr); + } + + /** + * Redraw all pending updates. + */ + protected void redraw() { + while(!updateQueue.isEmpty()) { + Visualization vis = updateQueue.pop(); + vis.incrementalRedraw(); + } + } + + /** + * Request a redraw of a visualization. + */ + public void requestRedraw(VisualizationTask task, Visualization vis) { + updateQueue.add(vis); + synchronizedRedraw(); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/DetailView.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/DetailView.java new file mode 100644 index 00000000..64359463 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/DetailView.java @@ -0,0 +1,422 @@ +package de.lmu.ifi.dbs.elki.visualization.gui.detail; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultListener; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizationListener; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGEffects; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Manages a detail view. + * + * @author Erich Schubert + * + * @apiviz.has Visualization + * @apiviz.has PlotItem + * @apiviz.uses VisualizerContext + * @apiviz.uses VisualizationTask + */ +public class DetailView extends VisualizationPlot implements ResultListener, VisualizationListener { + /** + * Class logger + */ + private static final Logging LOG = Logging.getLogger(DetailView.class); + + /** + * Meta information on the visualizers contained. + */ + private PlotItem item; + + /** + * Ratio of this view. + */ + double ratio = 1.0; + + /** + * The visualizer context + */ + VisualizerContext context; + + /** + * Map from tasks to visualizations. + */ + Map<VisualizationTask, Visualization> taskmap = new HashMap<>(); + + /** + * Map from visualizations to SVG layers. + */ + Map<Visualization, Element> layermap = new HashMap<>(); + + /** + * The created width + */ + private double width; + + /** + * The created height + */ + private double height; + + /** + * Pending refresh, for lazy refreshing + */ + AtomicReference<Runnable> pendingRefresh = new AtomicReference<>(null); + + /** + * Constructor. + * + * @param vis Visualizations to use + * @param ratio Plot ratio + */ + public DetailView(VisualizerContext context, PlotItem vis, double ratio) { + super(); + this.context = context; + this.item = new PlotItem(vis); // Clone! + this.ratio = ratio; + + this.item.sort(); + + // TODO: only do this when there is an interactive visualizer? + setDisableInteractions(true); + addBackground(context); + SVGEffects.addShadowFilter(this); + SVGEffects.addLightGradient(this); + + initialize(); + context.addVisualizationListener(this); + context.addResultListener(this); + // FIXME: add datastore listener, too? + } + + /** + * Create a background node. Note: don't call this at arbitrary times - the + * background may cover already drawn parts of the image! + * + * @param context + */ + private void addBackground(VisualizerContext context) { + // Make a background + CSSClass cls = new CSSClass(this, "background"); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, context.getStyleLibrary().getBackgroundColor(StyleLibrary.PAGE)); + Element bg = this.svgElement(SVGConstants.SVG_RECT_TAG); + SVGUtil.setAtt(bg, SVGConstants.SVG_X_ATTRIBUTE, "0"); + SVGUtil.setAtt(bg, SVGConstants.SVG_Y_ATTRIBUTE, "0"); + SVGUtil.setAtt(bg, SVGConstants.SVG_WIDTH_ATTRIBUTE, "100%"); + SVGUtil.setAtt(bg, SVGConstants.SVG_HEIGHT_ATTRIBUTE, "100%"); + SVGUtil.setAtt(bg, NO_EXPORT_ATTRIBUTE, NO_EXPORT_ATTRIBUTE); + addCSSClassOrLogError(cls); + SVGUtil.setCSSClass(bg, cls.getName()); + + // Note that we rely on this being called before any other drawing routines. + getRoot().appendChild(bg); + } + + private void initialize() { + // Try to keep the area approximately 1.0 + width = Math.sqrt(getRatio()); + height = 1.0 / width; + + ArrayList<Visualization> layers = new ArrayList<>(); + // TODO: center/arrange visualizations? + for(Iterator<VisualizationTask> tit = item.tasks.iterator(); tit.hasNext();) { + VisualizationTask task = tit.next(); + if(task.visible) { + Visualization v = instantiateVisualization(task); + if(v != null) { + layers.add(v); + taskmap.put(task, v); + layermap.put(v, v.getLayer()); + } + } + } + // Arrange + for(Visualization layer : layers) { + if(layer.getLayer() != null) { + getRoot().appendChild(layer.getLayer()); + } + else { + LOG.warning("NULL layer seen."); + } + } + + double ratio = width / height; + getRoot().setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm"); + getRoot().setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, (20 / ratio) + "cm"); + getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 " + width + " " + height); + + updateStyleElement(); + } + + /** + * Do a refresh (when visibilities have changed). + */ + private synchronized void refresh() { + pendingRefresh.set(null); // Clear + if(LOG.isDebuggingFine()) { + LOG.debugFine("Refresh in thread " + Thread.currentThread().getName()); + } + boolean updateStyle = false; + Iterator<Map.Entry<VisualizationTask, Visualization>> it = taskmap.entrySet().iterator(); + while(it.hasNext()) { + Entry<VisualizationTask, Visualization> ent = it.next(); + VisualizationTask task = ent.getKey(); + Visualization vis = ent.getValue(); + if(vis == null) { + vis = instantiateVisualization(task); + ent.setValue(vis); + } + Element prevlayer = layermap.get(vis); + Element layer = vis.getLayer(); + if(prevlayer == layer) { // Unchanged: + // Current visibility ("not hidden") + boolean isVisible = !SVGConstants.CSS_HIDDEN_VALUE.equals(layer.getAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)); + if(task.visible != isVisible) { + // scheduleUpdate(new AttributeModifier( + layer.setAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY, // + task.visible ? SVGConstants.CSS_VISIBLE_VALUE : SVGConstants.CSS_HIDDEN_VALUE); + } + } + else { + if(task.hasAnyFlags(VisualizationTask.FLAG_NO_EXPORT)) { + layer.setAttribute(NO_EXPORT_ATTRIBUTE, NO_EXPORT_ATTRIBUTE); + } + if(prevlayer == null) { + if(LOG.isDebuggingFine()) { + LOG.debugFine("New layer: " + task); + } + // Insert new! + // TODO: insert position! + getRoot().appendChild(layer); + } + else { + if(LOG.isDebuggingFine()) { + LOG.debugFine("Updated layer: " + task); + } + // Replace + final Node parent = prevlayer.getParentNode(); + if(parent != null) { + parent.replaceChild(/* new! */layer, /* old */prevlayer); + } + } + layermap.put(vis, layer); + updateStyle = true; + } + } + if(updateStyle) { + updateStyleElement(); + } + } + + /** + * Instantiate a visualization. + * + * @param task Task to instantiate + * @return Visualization + */ + private Visualization instantiateVisualization(VisualizationTask task) { + try { + Visualization v = task.getFactory().makeVisualization(task, this, width, height, item.proj); + if(task.hasAnyFlags(VisualizationTask.FLAG_NO_EXPORT)) { + v.getLayer().setAttribute(NO_EXPORT_ATTRIBUTE, NO_EXPORT_ATTRIBUTE); + } + return v; + } + catch(Exception e) { + if(LOG.isDebugging()) { + LOG.warning("Visualizer " + task.getFactory().getClass().getName() + " failed.", e); + } + else { + LOG.warning("Visualizer " + task.getFactory().getClass().getName() + " failed - enable debugging to see details: " + e.toString()); + } + } + return null; + } + + /** + * Cleanup function. To remove listeners. + */ + public void destroy() { + context.removeVisualizationListener(this); + context.removeResultListener(this); + for(Entry<VisualizationTask, Visualization> v : taskmap.entrySet()) { + Visualization vis = v.getValue(); + if(vis != null) { + vis.destroy(); + } + } + taskmap.clear(); + } + + @Override + public void dispose() { + destroy(); + super.dispose(); + } + + /** + * Get the plot ratio. + * + * @return the current ratio + */ + public double getRatio() { + return ratio; + } + + /** + * Set the plot ratio + * + * @param ratio the new ratio to set + */ + public void setRatio(double ratio) { + // TODO: trigger refresh? + this.ratio = ratio; + } + + /** + * Trigger a refresh. + */ + private void lazyRefresh() { + Runnable pr = new Runnable() { + @Override + public void run() { + if(DetailView.this.pendingRefresh.compareAndSet(this, null)) { + DetailView.this.refresh(); + } + } + }; + DetailView.this.pendingRefresh.set(pr); + scheduleUpdate(pr); + } + + @Override + public void resultAdded(Result child, Result parent) { + lazyRefresh(); + } + + @Override + public void resultChanged(Result current) { + lazyRefresh(); + } + + @Override + public void resultRemoved(Result child, Result parent) { + lazyRefresh(); + } + + @Override + public void visualizationChanged(VisualizationItem current) { + // Make sure we are affected: + if(!(current instanceof VisualizationTask)) { + return; + } + final VisualizationTask task = (VisualizationTask) current; + // Get the layer + Visualization vis = taskmap.get(task); + if(vis == null) { // Unknown only. + boolean include = false; + Hierarchy.Iter<Object> it = context.getVisHierarchy().iterAncestors(current); + for(; it.valid(); it.advance()) { + if((item.proj != null && item.proj.getProjector() == it.get()) || taskmap.containsKey(it.get())) { + include = true; + break; + } + } + if(!include) { + return; // Attached to different projection. + } + } + if(vis == null) { // New visualization + taskmap.put(task, null); + lazyRefresh(); + } + else { + Element prevlayer = layermap.get(vis); + Element layer = vis.getLayer(); + if(prevlayer != layer) { + lazyRefresh(); + } + else { + boolean isVisible = !SVGConstants.CSS_HIDDEN_VALUE.equals(layer.getAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)); + if(task.visible != isVisible) { + lazyRefresh(); // Visibility has changed. + } + } + } + } + + @Override + protected void redraw() { + boolean active = false; + while(!updateQueue.isEmpty()) { + Visualization vis = updateQueue.pop(); + if(!active) { + Element prev = layermap.get(vis); + vis.incrementalRedraw(); + final boolean changed = prev != vis.getLayer(); + if(LOG.isDebuggingFine() && changed) { + LOG.debugFine("Visualization " + vis + " changed."); + } + active |= changed; + } + else { + vis.incrementalRedraw(); + } + } + if(active || true) { + refresh(); + } + } + + /** + * Get the item visualized by this view. + * + * @return Plot item + */ + public PlotItem getPlotItem() { + return item; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/package-info.java new file mode 100755 index 00000000..22dfba6f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Classes for managing a detail view.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.gui.detail;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java new file mode 100644 index 00000000..16c59fee --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java @@ -0,0 +1,75 @@ +package de.lmu.ifi.dbs.elki.visualization.gui.overview; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.event.ActionEvent; + +import de.lmu.ifi.dbs.elki.visualization.gui.detail.DetailView; + +/** + * Event when a particular subplot was selected. Plots are currently identified + * by their coordinates on the screen. + * + * @author Erich Schubert + */ +public class DetailViewSelectedEvent extends ActionEvent { + /** + * Serial version + */ + private static final long serialVersionUID = 1L; + + /** + * Parent overview plot. + */ + OverviewPlot overview; + + /** + * Plot item selected + */ + PlotItem it; + + /** + * Constructor. To be called by OverviewPlot only! + * + * @param source source plot + * @param id ID + * @param command command that was invoked + * @param modifiers modifiers + * @param it Plot item selected + */ + public DetailViewSelectedEvent(OverviewPlot source, int id, String command, int modifiers, PlotItem it) { + super(source, id, command, modifiers); + this.overview = source; + this.it = it; + } + + /** + * Retrieve a materialized detail plot. + * + * @return materialized detail plot + */ + public DetailView makeDetailView() { + return overview.makeDetailView(it); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/LayerMap.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/LayerMap.java new file mode 100644 index 00000000..0cdbb29e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/LayerMap.java @@ -0,0 +1,160 @@ +package de.lmu.ifi.dbs.elki.visualization.gui.overview; +/* + 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 <http://www.gnu.org/licenses/>. + */ +import java.util.*; + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.utilities.pairs.Pair; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Class to help keeping track of the materialized layers of the different visualizations. + * + * @author Erich Schubert + * + * @apiviz.has PlotItem + * @apiviz.has VisualizationTask + */ +public class LayerMap { + /** + * The actual map + */ + private HashMap<Pair<PlotItem, VisualizationTask>, Pair<Element, Visualization>> map = new HashMap<>(); + + /** + * Helper function for building a key object + * + * @param item Plot item + * @param task Visualization Task + * @return Key + */ + private Pair<PlotItem, VisualizationTask> key(PlotItem item, VisualizationTask task) { + return new Pair<>(item, task); + } + + /** + * Helper function to build a value pair + * + * @param elem Container element + * @param vis Visualization + * @return Value object + */ + private Pair<Element, Visualization> value(Element elem, Visualization vis) { + return new Pair<>(elem, vis); + } + + /** + * Get the visualization referenced by a item/key combination. + * + * @param item Plot ttem + * @param task Visualization task + * @return Visualization + */ + public Visualization getVisualization(PlotItem item, VisualizationTask task) { + Pair<Element, Visualization> pair = map.get(key(item, task)); + if (pair == null) { + return null; + } else { + return pair.second; + } + } + + /** + * Get the container element referenced by a item/key combination. + * + * @param item Plot item + * @param task Visualization task + * @return Container element + */ + public Element getContainer(PlotItem item, VisualizationTask task) { + Pair<Element, Visualization> pair = map.get(key(item, task)); + if (pair == null) { + return null; + } else { + return pair.first; + } + } + + /** + * Iterate over values + * + * @return Value iterable + */ + public Iterable<Pair<Element, Visualization>> values() { + return map.values(); + } + + /** + * Clear a map + */ + public void clear() { + map.clear(); + } + + /** + * Put a new combination into the map. + * + * @param it Plot item + * @param task Visualization Task + * @param elem Container element + * @param vis Visualization + */ + public void put(PlotItem it, VisualizationTask task, Element elem, Visualization vis) { + map.put(key(it, task), value(elem, vis)); + } + + /** + * Remove a combination. + * + * @param it Plot item + * @param task Visualization task + * @return Previous value + */ + public Pair<Element, Visualization> remove(PlotItem it, VisualizationTask task) { + return map.remove(key(it, task)); + } + + /** + * Put a new item into the visualizations + * + * @param it Plot item + * @param task Visualization task + * @param pair Pair object + */ + public void put(PlotItem it, VisualizationTask task, Pair<Element, Visualization> pair) { + map.put(key(it, task), pair); + } + + /** + * Get a pair from the map + * + * @param it Plot item + * @param task Visualization task + * @return Pair object + */ + public Pair<Element, Visualization> get(PlotItem it, VisualizationTask task) { + return map.get(key(it, task)); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java new file mode 100644 index 00000000..23ab2c21 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java @@ -0,0 +1,661 @@ +package de.lmu.ifi.dbs.elki.visualization.gui.overview; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultListener; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.pairs.Pair; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizationListener; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.CSSHoverClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.gui.detail.DetailView; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGEffects; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Generate an overview plot for a set of visualizations. + * + * @author Erich Schubert + * @author Remigius Wojdanowski + * + * @apiviz.landmark + * @apiviz.has VisualizerContext + * @apiviz.composedOf RectangleArranger + * @apiviz.composedOf LayerMap + * @apiviz.has DetailViewSelectedEvent + * @apiviz.uses DetailView + */ +public class OverviewPlot implements ResultListener, VisualizationListener { + /** + * Our logging class + */ + private static final Logging LOG = Logging.getLogger(OverviewPlot.class); + + /** + * Event when the overview plot started refreshing. + */ + public static final String OVERVIEW_REFRESHING = "Overview refreshing"; + + /** + * Event when the overview plot was refreshed. + */ + public static final String OVERVIEW_REFRESHED = "Overview refreshed"; + + /** + * Draw red borders around items. + */ + private static final boolean DEBUG_LAYOUT = false; + + /** + * Visualizer context + */ + private VisualizerContext context; + + /** + * The SVG plot object. + */ + private VisualizationPlot plot; + + /** + * Map of coordinates to plots. + */ + protected RectangleArranger<PlotItem> plotmap; + + /** + * Action listeners for this plot. + */ + private ArrayList<ActionListener> actionListeners = new ArrayList<>(); + + /** + * Single view mode + */ + private boolean single; + + /** + * Screen size (used for thumbnail sizing) + */ + public int screenwidth = 2000; + + /** + * Screen size (used for thumbnail sizing) + */ + public int screenheight = 2000; + + /** + * React to mouse hover events + */ + private EventListener hoverer; + + /** + * Lookup + */ + private LayerMap vistoelem = new LayerMap(); + + /** + * Layer for plot thumbnail + */ + private Element plotlayer; + + /** + * Layer for hover elements + */ + private Element hoverlayer; + + /** + * The CSS class used on "selectable" rectangles. + */ + private CSSClass selcss; + + /** + * Screen ratio + */ + private double ratio = 1.0; + + /** + * Pending refresh, for lazy refreshing + */ + AtomicReference<Runnable> pendingRefresh = new AtomicReference<>(null); + + /** + * Reinitialize on refresh + */ + private boolean reinitOnRefresh = false; + + /** + * Constructor. + * + * @param context Visualizer context + * @param single Single view mode + */ + public OverviewPlot(VisualizerContext context, boolean single) { + super(); + this.context = context; + this.single = single; + + // Important: + // You still need to call: initialize(ratio); + } + + /** + * Recompute the layout of visualizations. + * + * @param width Initial width + * @param height Initial height + * @return Arrangement + */ + private RectangleArranger<PlotItem> arrangeVisualizations(double width, double height) { + if(!(width > 0. && height > 0.)) { + LOG.warning("No size information during arrange()", new Throwable()); + return new RectangleArranger<>(1., 1.); + } + RectangleArranger<PlotItem> plotmap = new RectangleArranger<>(width, height); + + Hierarchy<Object> vistree = context.getVisHierarchy(); + for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) { + if(!(iter2.get() instanceof Projector)) { + continue; + } + Projector p = (Projector) iter2.get(); + Collection<PlotItem> projs = p.arrange(context); + for(PlotItem it : projs) { + if(it.w <= 0.0 || it.h <= 0.0) { + LOG.warning("Plot item with improper size information: " + it); + continue; + } + plotmap.put(it.w, it.h, it); + } + } + + nextTask: for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) { + if(!(iter2.get() instanceof VisualizationTask)) { + continue; + } + VisualizationTask task = (VisualizationTask) iter2.get(); + if(!task.visible) { + continue; + } + for(Hierarchy.Iter<?> iter = vistree.iterParents(task); iter.valid(); iter.advance()) { + if(iter.get() instanceof Projector) { + continue nextTask; + } + } + if(task.reqwidth <= 0.0 || task.reqheight <= 0.0) { + LOG.warning("Task with improper size information: " + task); + continue; + } + PlotItem it = new PlotItem(task.reqwidth, task.reqheight, null); + it.tasks.add(task); + plotmap.put(it.w, it.h, it); + } + return plotmap; + } + + /** + * Initialize the plot. + * + * @param ratio Initial ratio + */ + public void initialize(double ratio) { + if(!(ratio > 0 && ratio < Double.POSITIVE_INFINITY)) { + LOG.warning("Invalid ratio: " + ratio, new Throwable()); + ratio = 1.4; + } + this.ratio = ratio; + if(plot != null) { + LOG.warning("Already initialized."); + lazyRefresh(); + return; + } + reinitialize(); + // register context listener + context.addResultListener(this); + context.addVisualizationListener(this); + } + + /** + * Refresh the overview plot. + */ + private synchronized void reinitialize() { + if(plot == null) { + initializePlot(); + } + else { + final ActionEvent ev = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, OVERVIEW_REFRESHING); + for(ActionListener actionListener : actionListeners) { + actionListener.actionPerformed(ev); + } + } + + // Detach existing elements: + for(Pair<Element, Visualization> pair : vistoelem.values()) { + SVGUtil.removeFromParent(pair.first); + } + plotmap = arrangeVisualizations(ratio, 1.0); + + recalcViewbox(); + final int thumbsize = (int) Math.max(screenwidth / plotmap.getWidth(), screenheight / plotmap.getHeight()); + // TODO: cancel pending thumbnail requests! + + // Replace the layer map + LayerMap oldlayers = vistoelem; + vistoelem = new LayerMap(); + + // Redo main layers + SVGUtil.removeFromParent(plotlayer); + SVGUtil.removeFromParent(hoverlayer); + plotlayer = plot.svgElement(SVGConstants.SVG_G_TAG); + hoverlayer = plot.svgElement(SVGConstants.SVG_G_TAG); + hoverlayer.setAttribute(SVGPlot.NO_EXPORT_ATTRIBUTE, SVGPlot.NO_EXPORT_ATTRIBUTE); + + // Redo the layout + for(Entry<PlotItem, double[]> e : plotmap.entrySet()) { + final double basex = e.getValue()[0]; + final double basey = e.getValue()[1]; + for(Iterator<PlotItem> iter = e.getKey().itemIterator(); iter.hasNext();) { + PlotItem it = iter.next(); + + boolean hasDetails = false; + // Container element for main plot item + Element g = plot.svgElement(SVGConstants.SVG_G_TAG); + SVGUtil.setAtt(g, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "translate(" + (basex + it.x) + " " + (basey + it.y) + ")"); + plotlayer.appendChild(g); + vistoelem.put(it, null, g, null); + // Add the actual tasks: + for(VisualizationTask task : it.tasks) { + if(!visibleInOverview(task)) { + continue; + } + hasDetails |= !task.hasAnyFlags(VisualizationTask.FLAG_NO_DETAIL); + Pair<Element, Visualization> pair = oldlayers.remove(it, task); + if(pair == null) { + pair = new Pair<>(null, null); + pair.first = plot.svgElement(SVGConstants.SVG_G_TAG); + } + if(pair.second == null) { + pair.second = embedOrThumbnail(thumbsize, it, task, pair.first); + } + g.appendChild(pair.first); + vistoelem.put(it, task, pair); + } + // When needed, add a hover effect + if(hasDetails && !single) { + Element hover = plot.svgRect(basex + it.x, basey + it.y, it.w, it.h); + SVGUtil.addCSSClass(hover, selcss.getName()); + // link hoverer. + EventTarget targ = (EventTarget) hover; + targ.addEventListener(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE, hoverer, false); + targ.addEventListener(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE, hoverer, false); + targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, hoverer, false); + targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, new SelectPlotEvent(it), false); + + hoverlayer.appendChild(hover); + } + } + } + for(Pair<Element, Visualization> pair : oldlayers.values()) { + if(pair.second != null) { + pair.second.destroy(); + } + } + plot.getRoot().appendChild(plotlayer); + plot.getRoot().appendChild(hoverlayer); + plot.updateStyleElement(); + + // Notify listeners. + final ActionEvent ev = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, OVERVIEW_REFRESHED); + for(ActionListener actionListener : actionListeners) { + actionListener.actionPerformed(ev); + } + } + + /** + * Initialize the SVG plot. + */ + private void initializePlot() { + plot = new VisualizationPlot(); + { // Add a background element: + CSSClass cls = new CSSClass(this, "background"); + final String bgcol = context.getStyleLibrary().getBackgroundColor(StyleLibrary.PAGE); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, bgcol); + plot.addCSSClassOrLogError(cls); + Element background = plot.svgElement(SVGConstants.SVG_RECT_TAG); + background.setAttribute(SVGConstants.SVG_X_ATTRIBUTE, "0"); + background.setAttribute(SVGConstants.SVG_Y_ATTRIBUTE, "0"); + background.setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "100%"); + background.setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "100%"); + SVGUtil.setCSSClass(background, cls.getName()); + // Don't export a white background: + if("white".equals(bgcol)) { + background.setAttribute(SVGPlot.NO_EXPORT_ATTRIBUTE, SVGPlot.NO_EXPORT_ATTRIBUTE); + } + plot.getRoot().appendChild(background); + } + { // setup the hover CSS classes. + selcss = new CSSClass(this, "s"); + if(DEBUG_LAYOUT) { + selcss.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_RED_VALUE); + selcss.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, .00001 * StyleLibrary.SCALE); + selcss.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, "0.5"); + } + selcss.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE); + selcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0"); + selcss.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + plot.addCSSClassOrLogError(selcss); + CSSClass hovcss = new CSSClass(this, "h"); + hovcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0.25"); + plot.addCSSClassOrLogError(hovcss); + // Hover listener. + hoverer = new CSSHoverClass(hovcss.getName(), null, true); + } + + // Disable Batik default interactions (zoom, rotate, etc.) + if(single) { + plot.setDisableInteractions(true); + } + SVGEffects.addShadowFilter(plot); + SVGEffects.addLightGradient(plot); + } + + /** + * Produce thumbnail for a visualizer. + * + * @param thumbsize Thumbnail size + * @param it Plot item + * @param task Task + * @param parent Parent element to draw to + */ + private Visualization embedOrThumbnail(final int thumbsize, PlotItem it, VisualizationTask task, Element parent) { + final Visualization vis; + if(!single) { + vis = task.getFactory().makeVisualizationOrThumbnail(task, plot, it.w, it.h, it.proj, thumbsize); + } + else { + vis = task.getFactory().makeVisualization(task, plot, it.w, it.h, it.proj); + } + if(vis == null || vis.getLayer() == null) { + LoggingUtil.warning("Visualization returned empty layer: " + vis); + return vis; + } + if(task.hasAnyFlags(VisualizationTask.FLAG_NO_EXPORT)) { + vis.getLayer().setAttribute(SVGPlot.NO_EXPORT_ATTRIBUTE, SVGPlot.NO_EXPORT_ATTRIBUTE); + } + parent.appendChild(vis.getLayer()); + return vis; + } + + /** + * Do a refresh (when visibilities have changed). + */ + synchronized void refresh() { + if(reinitOnRefresh) { + LOG.debug("Reinitialize in thread " + Thread.currentThread().getName()); + reinitialize(); + reinitOnRefresh = false; + return; + } + synchronized(plot) { + boolean refreshcss = false; + if(plotmap == null) { + LOG.warning("Plotmap is null", new Throwable()); + } + final int thumbsize = (int) Math.max(screenwidth / plotmap.getWidth(), screenheight / plotmap.getHeight()); + for(PlotItem pi : plotmap.keySet()) { + for(Iterator<PlotItem> iter = pi.itemIterator(); iter.hasNext();) { + PlotItem it = iter.next(); + + for(Iterator<VisualizationTask> tit = it.tasks.iterator(); tit.hasNext();) { + VisualizationTask task = tit.next(); + Pair<Element, Visualization> pair = vistoelem.get(it, task); + // New task? + if(pair == null) { + if(visibleInOverview(task)) { + pair = new Pair<>(null, null); + pair.first = plot.svgElement(SVGConstants.SVG_G_TAG); + pair.second = embedOrThumbnail(thumbsize, it, task, pair.first); + vistoelem.get(it, null).first.appendChild(pair.first); + vistoelem.put(it, task, pair); + refreshcss = true; + } + } + else { + if(visibleInOverview(task)) { + // unhide if hidden. + if(pair.first.hasAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)) { + pair.first.removeAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY); + } + } + else { + // hide if there is anything to hide. + if(pair.first != null && pair.first.hasChildNodes()) { + pair.first.setAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY, SVGConstants.CSS_HIDDEN_VALUE); + } + } + // TODO: unqueue pending thumbnails + } + } + } + } + if(refreshcss) { + plot.updateStyleElement(); + } + } + } + + /** + * Test whether a task should be displayed in the overview plot. + * + * @param task Task to display + * @return visibility + */ + protected boolean visibleInOverview(VisualizationTask task) { + if(single) { + return task.visible && !task.hasAnyFlags(VisualizationTask.FLAG_NO_EMBED); + } + return task.visible && !task.hasAnyFlags(VisualizationTask.FLAG_NO_THUMBNAIL); + } + + /** + * Recompute the view box of the plot. + */ + private void recalcViewbox() { + final Element root = plot.getRoot(); + // Reset plot attributes + SVGUtil.setAtt(root, SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm"); + SVGUtil.setAtt(root, SVGConstants.SVG_HEIGHT_ATTRIBUTE, SVGUtil.fmt(20 * plotmap.getHeight() / plotmap.getWidth()) + "cm"); + String vb = "0 0 " + SVGUtil.fmt(plotmap.getWidth()) + " " + SVGUtil.fmt(plotmap.getHeight()); + SVGUtil.setAtt(root, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, vb); + } + + /** + * Event triggered when a plot was selected. + * + * @param it Plot item selected + * @return sub plot + */ + public DetailView makeDetailView(PlotItem it) { + return new DetailView(context, it, ratio); + } + + /** + * Adds an {@link ActionListener} to the plot. + * + * @param actionListener the {@link ActionListener} to be added + */ + public void addActionListener(ActionListener actionListener) { + actionListeners.add(actionListener); + } + + /** + * When a subplot was selected, forward the event to listeners. + * + * @param it PlotItem selected + */ + protected void triggerSubplotSelectEvent(PlotItem it) { + // forward event to all listeners. + for(ActionListener actionListener : actionListeners) { + actionListener.actionPerformed(new DetailViewSelectedEvent(this, ActionEvent.ACTION_PERFORMED, null, 0, it)); + } + } + + /** + * Event when a plot was selected. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public class SelectPlotEvent implements EventListener { + /** + * Plot item clicked + */ + PlotItem it; + + /** + * Constructor. + * + * @param it Item that was clicked + */ + public SelectPlotEvent(PlotItem it) { + super(); + this.it = it; + } + + @Override + public void handleEvent(Event evt) { + triggerSubplotSelectEvent(it); + } + } + + /** + * Destroy this overview plot. + */ + public void destroy() { + context.removeVisualizationListener(this); + context.removeResultListener(this); + plot.dispose(); + } + + /** + * Get the SVGPlot object. + * + * @return SVG plot + */ + public SVGPlot getPlot() { + return plot; + } + + /** + * @return the ratio + */ + public double getRatio() { + return ratio; + } + + /** + * @param ratio the ratio to set + */ + public void setRatio(double ratio) { + if(ratio != this.ratio) { + this.ratio = ratio; + reinitOnRefresh = true; + lazyRefresh(); + } + } + + /** + * Trigger a redraw, but avoid excessive redraws. + */ + public final void lazyRefresh() { + if(plot == null) { + LOG.warning("'lazyRefresh' called before initialized!"); + } + LOG.debug("Scheduling refresh."); + Runnable pr = new Runnable() { + @Override + public void run() { + if(OverviewPlot.this.pendingRefresh.compareAndSet(this, null)) { + OverviewPlot.this.refresh(); + } + } + }; + OverviewPlot.this.pendingRefresh.set(pr); + plot.scheduleUpdate(pr); + } + + @Override + public void resultAdded(Result child, Result parent) { + lazyRefresh(); + } + + @Override + public void resultChanged(Result current) { + lazyRefresh(); + } + + @Override + public void resultRemoved(Result child, Result parent) { + lazyRefresh(); + } + + @Override + public void visualizationChanged(VisualizationItem child) { + boolean isProjected = false; + for(Hierarchy.Iter<Object> iter = context.getVisHierarchy().iterParents(child); iter.valid(); iter.advance()) { + final Object o = iter.get(); + if(o instanceof Projector) { + isProjected = true; + break; + } + } + if(!isProjected) { + reinitOnRefresh = true; + } + lazyRefresh(); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java new file mode 100644 index 00000000..b95ef7e2 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java @@ -0,0 +1,227 @@ +package de.lmu.ifi.dbs.elki.visualization.gui.overview; + +import java.util.ArrayList; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; + +/** + * Item to collect visualization tasks on a specific position on the plot map. + * + * Note: this is a {@code LinkedList<VisualizationTask>}! + * + * @author Erich Schubert + * + * @apiviz.composedOf Projection + * @apiviz.composedOf VisualizationTask + * @apiviz.composedOf PlotItem + */ +public class PlotItem { + /** + * Position: x + */ + public final double x; + + /** + * Position: y + */ + public final double y; + + /** + * Size: width + */ + public final double w; + + /** + * Size: height + */ + public final double h; + + /** + * Projection (may be {@code null}!) + */ + public final Projection proj; + + /** + * The visualization tasks at this location + */ + public List<VisualizationTask> tasks = new LinkedList<>(); + + /** + * Subitems to plot + */ + public Collection<PlotItem> subitems = new LinkedList<>(); + + /** + * Constructor. + * + * @param w Position: w + * @param h Position: h + * @param proj Projection + */ + public PlotItem(double w, double h, Projection proj) { + this(0, 0, w, h, proj); + } + + /** + * Constructor. + * + * @param x Position: x + * @param y Position: y + * @param w Position: w + * @param h Position: h + * @param proj Projection + */ + public PlotItem(double x, double y, double w, double h, Projection proj) { + super(); + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.proj = proj; + } + + /** + * Clone constructor. + * + * @param vis Existing plot item. + */ + public PlotItem(PlotItem vis) { + super(); + this.x = vis.x; + this.y = vis.y; + this.w = vis.w; + this.h = vis.h; + this.proj = vis.proj; + this.tasks = new ArrayList<>(vis.tasks); + this.subitems = new ArrayList<>(vis.subitems.size()); + for(PlotItem s : vis.subitems) { + this.subitems.add(new PlotItem(s)); + } + } + + /** + * Sort all visualizers for their proper drawing order + */ + public void sort() { + Collections.sort(tasks); + for(PlotItem subitem : subitems) { + subitem.sort(); + } + } + + /** + * Add a task to the item. + * + * @param task Task to add + */ + public void add(VisualizationTask task) { + tasks.add(task); + } + + /** + * Number of tasks in this item. + * + * @return Number of tasks. + */ + public int taskSize() { + return tasks.size(); + } + + /** + * Iterate (recursively) over all plot items, including itself. + * + * @return Iterator + */ + public Iterator<PlotItem> itemIterator() { + return new ItmItr(); + } + + @Override + public String toString() { + return "PlotItem [x=" + x + ", y=" + y + ", w=" + w + ", h=" + h + ",proj=" + proj + "]"; + } + + /** + * Recursive iterator + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + private class ItmItr implements Iterator<PlotItem> { + PlotItem next; + + Iterator<PlotItem> cur; + + Iterator<PlotItem> sub; + + /** + * Constructor. + */ + public ItmItr() { + super(); + this.next = PlotItem.this; + this.cur = null; + this.sub = subitems.iterator(); + } + + @Override + public boolean hasNext() { + if(next != null) { + return true; + } + if(cur != null && cur.hasNext()) { + next = cur.next(); + return true; + } + if(sub.hasNext()) { + cur = sub.next().itemIterator(); + return hasNext(); + } + return false; + } + + @Override + public PlotItem next() { + hasNext(); + PlotItem ret = next; + next = null; + return ret; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java new file mode 100644 index 00000000..ef05b6c0 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java @@ -0,0 +1,546 @@ +package de.lmu.ifi.dbs.elki.visualization.gui.overview; +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Level; + +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.logging.LoggingConfiguration; +import de.lmu.ifi.dbs.elki.utilities.datastructures.arraylike.DoubleArray; + +/** + * This is a rather naive rectangle arrangement class. It will try to place + * rectangles on a canvas while maintaining the canvas size ratio as good as + * possible. It does not do an exhaustive search for optimizing the layout, but + * a greedy placement strategy, extending the canvas as little as possible. + * + * @author Erich Schubert + * + * @param <T> Key type + */ +public class RectangleArranger<T> { + /** + * Logging class + */ + private static final Logging LOG = Logging.getLogger(RectangleArranger.class); + + /** + * Target height/width ratio + */ + private double ratio = 1.0; + + /** + * Width + */ + private double twidth = 1.0; + + /** + * Height + */ + private double theight = 1.0; + + /** + * Column widths + */ + private DoubleArray widths = new DoubleArray(); + + /** + * Column heights + */ + private DoubleArray heights = new DoubleArray(); + + /** + * Map indicating which cells are used. + */ + private ArrayList<ArrayList<Object>> usage = new ArrayList<>(); + + /** + * Data + */ + private Map<T, double[]> map = new HashMap<>(); + + /** + * Constructor. + * + * @param ratio + */ + public RectangleArranger(double ratio) { + this(ratio, 1.0); + } + + /** + * Constructor. + * + * @param width Canvas width + * @param height Canvas height + */ + public RectangleArranger(double width, double height) { + this.ratio = width / height; + this.twidth = width; + this.theight = height; + this.widths.add(width); + this.heights.add(height); + // setup usage matrix + ArrayList<Object> u = new ArrayList<>(); + u.add(null); + this.usage.add(u); + assertConsistent(); + } + + /** + * Add a new recangle. + * + * @param w Width + * @param h Height + * @param data Data object to add (key) + */ + public void put(double w, double h, T data) { + if(LOG.isDebuggingFinest()) { + LOG.finest("Add: " + w + "x" + h); + } + final int cols = widths.size(); + final int rows = heights.size(); + + int bestsx = -1; + int bestsy = -1; + int bestex = cols - 1; + int bestey = -1; + double bestwi; + double besthi; + double bestinc; + // Baseline: grow by adding to the top or to the right. + { + double i1 = computeIncreaseArea(w, Math.max(0, h - theight)); + double i2 = computeIncreaseArea(Math.max(0, w - twidth), h); + if(i1 < i2) { + bestwi = w; + besthi = Math.max(0, h - theight); + bestinc = i1; + } + else { + bestwi = Math.max(0, w - twidth); + besthi = h; + bestinc = i2; + } + } + // Find position with minimum increase + for(int sy = 0; sy < rows; sy++) { + for(int sx = 0; sx < cols; sx++) { + if(usage.get(sy).get(sx) != null) { + continue; + } + // Start with single cell + double avw = widths.get(sx); + double avh = heights.get(sy); + int ex = sx; + int ey = sy; + while(avw < w || avh < h) { + // Grow width first + if(avw / avh < w / h) { + if(avw < w && ex + 1 < cols) { + boolean ok = true; + // All unused? + for(int y = sy; y <= ey; y++) { + if(usage.get(y).get(ex + 1) != null) { + ok = false; + } + } + if(ok) { + ex += 1; + avw += widths.get(ex); + continue; + } + } + if(avh < h && ey + 1 < rows) { + boolean ok = true; + // All unused? + for(int x = sx; x <= ex; x++) { + if(usage.get(ey + 1).get(x) != null) { + ok = false; + } + } + if(ok) { + ey += 1; + avh += heights.get(ey); + continue; + } + } + } + else { // Grow height first + if(avh < h && ey + 1 < rows) { + boolean ok = true; + // All unused? + for(int x = sx; x <= ex; x++) { + if(usage.get(ey + 1).get(x) != null) { + ok = false; + } + } + if(ok) { + ey += 1; + avh += heights.get(ey); + continue; + } + } + if(avw < w && ex + 1 < cols) { + boolean ok = true; + // All unused? + for(int y = sy; y <= ey; y++) { + if(usage.get(y).get(ex + 1) != null) { + ok = false; + } + } + if(ok) { + ex += 1; + avw += widths.get(ex); + continue; + } + } + } + break; + } + // Good match, or extension possible? + if(avw < w && ex < cols - 1) { + continue; + } + if(avh < h && ey < rows - 1) { + continue; + } + // Compute increase: + double winc = Math.max(0.0, w - avw); + double hinc = Math.max(0.0, h - avh); + double inc = computeIncreaseArea(winc, hinc); + + if(LOG.isDebuggingFinest()) { + LOG.debugFinest("Candidate: " + sx + "," + sy + " - " + ex + "," + ey + ": " + avw + "x" + avh + " " + inc); + } + if(inc < bestinc) { + bestinc = inc; + bestsx = sx; + bestsy = sy; + bestex = ex; + bestey = ey; + bestwi = w - avw; + besthi = h - avh; + } + if(inc == 0) { + // Can't find better + // TODO: try to do less splitting maybe? + break; + } + } + assert assertConsistent(); + } + if(LOG.isDebuggingFinest()) { + LOG.debugFinest("Best: " + bestsx + "," + bestsy + " - " + bestex + "," + bestey + " inc: " + bestwi + "x" + besthi + " " + bestinc); + } + // Need to increase the total area + if(bestinc > 0) { + assert(bestex == cols - 1 || bestey == rows - 1); + double inc = Math.max(bestwi, besthi * ratio); + resize(inc); + + // Resubmit + put(w, h, data); + return; + } + // Need to split a column. + // TODO: find best column to split. Currently: last + if(bestwi < 0.0) { + splitCol(bestex, -bestwi); + bestwi = 0.0; + } + // Need to split a row. + // TODO: find best row to split. Currently: last + if(besthi < 0.0) { + splitRow(bestey, -besthi); + besthi = 0.0; + } + for(int x = bestsx; x <= bestex; x++) { + for(int y = bestsy; y <= bestey; y++) { + usage.get(y).set(x, data); + } + } + double xpos = 0.0; + double ypos = 0.0; + { + for(int x = 0; x < bestsx; x++) { + xpos += widths.get(x); + } + for(int y = 0; y < bestsy; y++) { + ypos += heights.get(y); + } + } + map.put(data, new double[] { xpos, ypos, w, h }); + if(LOG.isDebuggingFinest()) { + logSizes(); + } + } + + protected double computeIncreaseArea(double winc, double hinc) { + double inc = Math.max(winc, hinc * ratio); + inc = inc * (hinc + inc / ratio + winc / ratio); + return inc; + } + + protected void splitRow(int bestey, double besthi) { + assert(bestey < heights.size()); + if(heights.get(bestey) - besthi <= Double.MIN_NORMAL) { + return; + } + if(LOG.isDebuggingFine()) { + LOG.debugFine("Split row " + bestey); + } + heights.insert(bestey + 1, besthi); + heights.set(bestey, heights.get(bestey) - besthi); + // Update used map + usage.add(bestey + 1, new ArrayList<>(usage.get(bestey))); + } + + protected void splitCol(int bestex, double bestwi) { + assert(bestex < widths.size()); + if(widths.get(bestex) - bestwi <= Double.MIN_NORMAL) { + return; + } + final int rows = heights.size(); + if(LOG.isDebuggingFine()) { + LOG.debugFine("Split column " + bestex); + } + widths.insert(bestex + 1, bestwi); + widths.set(bestex, widths.get(bestex) - bestwi); + // Update used map + for(int y = 0; y < rows; y++) { + usage.get(y).add(bestex + 1, usage.get(y).get(bestex)); + } + assert assertConsistent(); + } + + private void resize(double inc) { + final int cols = widths.size(); + final int rows = heights.size(); + if(LOG.isDebuggingFine()) { + LOG.debugFine("Resize by " + inc + "x" + (inc / ratio)); + if(LOG.isDebuggingFinest()) { + logSizes(); + } + } + // TODO: if the last row or column is empty, we can do this simpler + widths.add(inc); + twidth += inc; + heights.add(inc / ratio); + theight += inc / ratio; + // Add column: + for(int y = 0; y < rows; y++) { + usage.get(y).add(null); + } + // Add row: + { + ArrayList<Object> row = new ArrayList<>(); + for(int x = 0; x <= cols; x++) { + row.add(null); + } + usage.add(row); + } + assert assertConsistent(); + if(LOG.isDebuggingFinest()) { + logSizes(); + } + } + + /** + * Get the position data of the object + * + * @param object Query object + * @return Position information: x,y,w,h + */ + public double[] get(T object) { + double[] v = map.get(object); + if(v == null) { + return null; + } + return v.clone(); + } + + private boolean assertConsistent() { + final int cols = widths.size(); + final int rows = heights.size(); + { + double wsum = 0.0; + for(int x = 0; x < cols; x++) { + assert(widths.get(x) > 0) : "Non-positive width: " + widths.get(x) + " at " + x; + wsum += widths.get(x); + } + assert(Math.abs(wsum - twidth) < 1E-10); + } + { + double hsum = 0.0; + for(int y = 0; y < rows; y++) { + assert(heights.get(y) > 0) : "Non-positive height: " + heights.get(y) + " at " + y; + hsum += heights.get(y); + } + assert(Math.abs(hsum - theight) < 1E-10); + } + { + assert(usage.size() == rows); + for(int y = 0; y < rows; y++) { + assert(usage.get(y).size() == cols); + } + } + return true; + } + + /** + * Debug logging + */ + protected void logSizes() { + StringBuilder buf = new StringBuilder(); + final int cols = widths.size(); + final int rows = heights.size(); + { + buf.append("Widths: "); + for(int x = 0; x < cols; x++) { + if(x > 0) { + buf.append(", "); + } + buf.append(widths.get(x)); + } + buf.append('\n'); + } + { + buf.append("Heights: "); + for(int y = 0; y < rows; y++) { + if(y > 0) { + buf.append(", "); + } + buf.append(heights.get(y)); + } + buf.append('\n'); + } + { + for(int y = 0; y < rows; y++) { + for(int x = 0; x < cols; x++) { + buf.append(usage.get(y).get(x) != null ? "X" : "_"); + } + buf.append("|\n"); + } + for(int x = 0; x < cols; x++) { + buf.append('-'); + } + buf.append("+\n"); + } + LOG.debug(buf); + } + + /** + * Compute the relative fill. Useful for triggering a relayout if the relative + * fill is not satisfactory. + * + * @return relative fill + */ + public double relativeFill() { + double acc = 0.0; + final int cols = widths.size(); + final int rows = heights.size(); + { + for(int y = 0; y < rows; y++) { + for(int x = 0; x < cols; x++) { + if(usage.get(y).get(x) != null) { + acc += widths.get(x) * heights.get(y); + } + } + } + } + return acc / (twidth * theight); + } + + /** + * Get the total canvas width + * + * @return Width + */ + public double getWidth() { + return twidth; + } + + /** + * Get the total canvas height + * + * @return Height + */ + public double getHeight() { + return theight; + } + + /** + * The items contained in the map. + * + * @return entry set + */ + public Set<Entry<T, double[]>> entrySet() { + return Collections.unmodifiableSet(map.entrySet()); + } + + /** + * The item keys contained in the map. + * + * @return key set + */ + public Set<T> keySet() { + return Collections.unmodifiableSet(map.keySet()); + } + + /** + * Test method. + * + * @param args + */ + public static void main(String[] args) { + LoggingConfiguration.setLevelFor(RectangleArranger.class.getName(), Level.FINEST.getName()); + RectangleArranger<String> r = new RectangleArranger<>(1.3); + r.put(4., 1., "Histogram"); + r.put(4., 4., "3D view"); + r.put(1., 1., "Meta 1"); + r.put(1., 1., "Meta 2"); + r.put(1., 1., "Meta 3"); + r.put(2., 2., "Meta 4"); + r.put(2., 2., "Meta 5"); + + r = new RectangleArranger<>(3., 3.); + r.put(1., 2., "A"); + r.put(2., 1., "B"); + r.put(1., 2., "C"); + r.put(2., 1., "D"); + r.put(2., 2., "E"); + + r = new RectangleArranger<>(4 - 2.6521739130434785); + r.put(4., .5, "A"); + r.put(4., 3., "B"); + r.put(4., 1., "C"); + r.put(1., .1, "D"); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/package-info.java new file mode 100755 index 00000000..b42202bb --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/package-info.java @@ -0,0 +1,28 @@ +/** + * <p>Classes for managing the overview plot.</p> + * + * @apiviz.exclude java.awt.event.* + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.gui.overview; diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/package-info.java new file mode 100755 index 00000000..5d7437b5 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Package to provide a visualization GUI.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.gui;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSCut.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSCut.java new file mode 100644 index 00000000..d85a9415 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSCut.java @@ -0,0 +1,101 @@ +package de.lmu.ifi.dbs.elki.visualization.opticsplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.model.ClusterModel; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDVar; +import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs; + +/** + * Compute a partitioning from an OPTICS plot by doing a horizontal cut. + * + * @author Heidi Kolb + * @author Erich Schubert + * + * @apiviz.uses ClusterOrder + */ +// TODO: add non-flat clusterings +public class OPTICSCut { + /** + * Compute an OPTICS cut clustering + * + * @param co Cluster order result + * @param epsilon Epsilon value for cut + * @return New partitioning clustering + */ + public static <E extends ClusterOrder> Clustering<Model> makeOPTICSCut(E co, double epsilon) { + // Clustering model we are building + Clustering<Model> clustering = new Clustering<>("OPTICS Cut Clustering", "optics-cut"); + // Collects noise elements + ModifiableDBIDs noise = DBIDUtil.newHashSet(); + + double lastDist = Double.MAX_VALUE; + double actDist = Double.MAX_VALUE; + + // Current working set + ModifiableDBIDs current = DBIDUtil.newHashSet(); + + // TODO: can we implement this more nicely with a 1-lookahead? + DBIDVar prev = DBIDUtil.newVar(); + for(DBIDIter it = co.iter(); it.valid(); prev.set(it), it.advance()) { + lastDist = actDist; + actDist = co.getReachability(it); + + if(actDist <= epsilon) { + // the last element before the plot drops belongs to the cluster + if(lastDist > epsilon && prev.isSet()) { + // So un-noise it + noise.remove(prev); + // Add it to the cluster + current.add(prev); + } + current.add(it); + } + else { + // 'Finish' the previous cluster + if(!current.isEmpty()) { + // TODO: do we want a minpts restriction? + // But we get have only core points guaranteed anyway. + clustering.addToplevelCluster(new Cluster<Model>(current, ClusterModel.CLUSTER)); + current = DBIDUtil.newHashSet(); + } + // Add to noise + noise.add(it); + } + } + // Any unfinished cluster will also be added + if(!current.isEmpty()) { + clustering.addToplevelCluster(new Cluster<Model>(current, ClusterModel.CLUSTER)); + } + // Add noise + clustering.addToplevelCluster(new Cluster<Model>(noise, true, ClusterModel.CLUSTER)); + return clustering; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSPlot.java new file mode 100644 index 00000000..c25a7961 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSPlot.java @@ -0,0 +1,301 @@ +package de.lmu.ifi.dbs.elki.visualization.opticsplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.math.DoubleMinMax; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; + +/** + * Class to produce an OPTICS plot image. + * + * @author Erich Schubert + * + * @apiviz.composedOf LinearScale + * @apiviz.has ClusterOrder oneway - - renders + */ +public class OPTICSPlot implements Result { + /** + * Logger + */ + private static final Logging LOG = Logging.getLogger(OPTICSPlot.class); + + /** + * Minimum and maximum vertical resolution. + */ + private static final int MIN_HEIGHT = 25, MAX_HEIGHT = 300; + + /** + * Scale to use + */ + LinearScale scale; + + /** + * Width of plot + */ + int width; + + /** + * Height of plot + */ + int height; + + /** + * Ratio of plot + */ + double ratio; + + /** + * The result to plot. + */ + final ClusterOrder co; + + /** + * Color adapter to use + */ + final StylingPolicy colors; + + /** + * The Optics plot. + */ + protected RenderedImage plot; + + /** + * The plot number for Batik + */ + protected int plotnum = -1; + + /** + * Constructor, with automatic distance adapter detection. + * + * @param co Cluster order to plot. + * @param colors Coloring strategy + */ + public OPTICSPlot(ClusterOrder co, StylingPolicy colors) { + super(); + this.co = co; + this.colors = colors; + } + + /** + * Trigger a redraw of the OPTICS plot + */ + public void replot() { + width = co.size(); + height = (int) Math.ceil(width * .2); + ratio = width / (double) height; + height = height < MIN_HEIGHT ? MIN_HEIGHT : height > MAX_HEIGHT ? MAX_HEIGHT : height; + if(scale == null) { + scale = computeScale(co); + } + + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + int x = 0; + for(DBIDIter it = co.iter(); it.valid(); it.advance()) { + double reach = co.getReachability(it); + final int y = scaleToPixel(reach); + try { + int col = colors.getColorForDBID(it); + for(int y2 = height - 1; y2 >= y; y2--) { + img.setRGB(x, y2, col); + } + } + catch(ArrayIndexOutOfBoundsException e) { + LOG.error("Plotting out of range: " + x + "," + y + " >= " + width + "x" + height); + } + x++; + } + + plot = img; + } + + /** + * Scale a reachability distance to a pixel value. + * + * @param reach Reachability + * @return Pixel value. + */ + public int scaleToPixel(double reach) { + return (Double.isInfinite(reach) || Double.isNaN(reach)) ? 0 : // + (int) Math.round(scale.getScaled(reach, height - .5, .5)); + } + + /** + * Scale a pixel value to a reachability + * + * @param y Pixel value + * @return Reachability + */ + public double scaleFromPixel(double y) { + return scale.getUnscaled((y - .5) / (height - 1.)); + } + + /** + * Compute the scale (value range) + * + * @param order Cluster order to process + * @return Scale for value range of cluster order + */ + protected LinearScale computeScale(ClusterOrder order) { + DoubleMinMax range = new DoubleMinMax(); + // calculate range + for(DBIDIter it = order.iter(); it.valid(); it.advance()) { + final double reach = co.getReachability(it); + if(reach < Double.POSITIVE_INFINITY) { + range.put(reach); + } + } + // Ensure we have a valid range + if(!range.isValid()) { + range.put(0.0); + range.put(1.0); + } + return new LinearScale(range.getMin(), range.getMax()); + } + + /** + * @return the scale + */ + public LinearScale getScale() { + if(plot == null) { + replot(); + } + return scale; + } + + /** + * @return the width + */ + public int getWidth() { + if(plot == null) { + replot(); + } + return width; + } + + /** + * @return the height + */ + public int getHeight() { + if(plot == null) { + replot(); + } + return height; + } + + /** + * Get width-to-height ratio of image. + * + * @return {@code width / height} + */ + public double getRatio() { + if(plot == null) { + replot(); + } + return ratio; + } + + /** + * Get the OPTICS plot. + * + * @return plot image + */ + public synchronized RenderedImage getPlot() { + if(plot == null) { + replot(); + } + return plot; + } + + /** + * Free memory used by rendered image. + */ + public void forgetRenderedImage() { + plotnum = -1; + plot = null; + } + + /** + * Get the SVG registered plot number + * + * @return Plot URI + */ + public String getSVGPlotURI() { + if(plotnum < 0) { + plotnum = ThumbnailRegistryEntry.registerImage(plot); + } + return ThumbnailRegistryEntry.INTERNAL_PREFIX + plotnum; + } + + @Override + public String getLongName() { + return "OPTICS Plot"; + } + + @Override + public String getShortName() { + return "optics plot"; + } + + /** + * Static method to find an optics plot for a result, or to create a new one + * using the given context. + * + * @param co Cluster order + * @param context Context (for colors and reference clustering) + * + * @return New or existing optics plot + */ + public static OPTICSPlot plotForClusterOrder(ClusterOrder co, VisualizerContext context) { + // Check for an existing plot + // ArrayList<OPTICSPlot<D>> plots = ResultUtil.filterResults(co, + // OPTICSPlot.class); + // if (plots.size() > 0) { + // return plots.get(0); + // } + final StylingPolicy policy = context.getStylingPolicy(); + OPTICSPlot opticsplot = new OPTICSPlot(co, policy); + // co.addChildResult(opticsplot); + return opticsplot; + } + + /** + * Get the cluster order we are attached to. + * + * @return Cluster order + */ + public ClusterOrder getClusterOrder() { + return co; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/package-info.java new file mode 100644 index 00000000..c39c344b --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Code for drawing OPTICS plots</p> + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.opticsplot;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/package-info.java new file mode 100644 index 00000000..98345a5e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/package-info.java @@ -0,0 +1,29 @@ +/** + * <p>Visualization package of ELKI.</p> + * + * @apiviz.exclude elki.utilities + * @apiviz.exclude java.lang.* + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization; diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractFullProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractFullProjection.java new file mode 100644 index 00000000..4c57a6f0 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractFullProjection.java @@ -0,0 +1,232 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; + +/** + * Abstract base class for full projections. + * + * Note: the full projection API may be removed at some point, unless we find a + * clear use case that cannot be done by the low level fast projections. + * + * @author Erich Schubert + */ +public abstract class AbstractFullProjection extends AbstractProjection implements FullProjection { + /** + * Constructor. + * + * @param p Projector + * @param scales Scales + */ + public AbstractFullProjection(Projector p, LinearScale[] scales) { + super(p, scales); + } + + /** + * Project a data vector from data space to scaled space. + * + * @param data vector in data space + * @return vector in scaled space + */ + @Override + public double[] projectDataToScaledSpace(NumberVector data) { + final int dim = data.getDimensionality(); + double[] vec = new double[dim]; + for(int d = 0; d < dim; d++) { + vec[d] = scales[d].getScaled(data.doubleValue(d)); + } + return vec; + } + + /** + * Project a data vector from data space to scaled space. + * + * @param data vector in data space + * @return vector in scaled space + */ + @Override + public double[] projectDataToScaledSpace(double[] data) { + final int dim = data.length; + double[] dst = new double[dim]; + for(int d = 0; d < dim; d++) { + dst[d] = scales[d].getScaled(data[d]); + } + return dst; + } + + /** + * Project a relative data vector from data space to scaled space. + * + * @param data relative vector in data space + * @return relative vector in scaled space + */ + @Override + public double[] projectRelativeDataToScaledSpace(NumberVector data) { + final int dim = data.getDimensionality(); + double[] vec = new double[dim]; + for(int d = 0; d < dim; d++) { + vec[d] = scales[d].getRelativeScaled(data.doubleValue(d)); + } + return vec; + } + + /** + * Project a relative data vector from data space to scaled space. + * + * @param data relative vector in data space + * @return relative vector in scaled space + */ + @Override + public double[] projectRelativeDataToScaledSpace(double[] data) { + final int dim = data.length; + double[] dst = new double[dim]; + for(int d = 0; d < dim; d++) { + dst[d] = scales[d].getRelativeScaled(data[d]); + } + return dst; + } + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + @Override + public double[] projectDataToRenderSpace(NumberVector data) { + return projectScaledToRender(projectDataToScaledSpace(data)); + } + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + @Override + public double[] projectDataToRenderSpace(double[] data) { + return projectScaledToRender(projectDataToScaledSpace(data)); + } + + /** + * Project a relative data vector from data space to rendering space. + * + * @param data relative vector in data space + * @return relative vector in rendering space + */ + @Override + public double[] projectRelativeDataToRenderSpace(NumberVector data) { + return projectRelativeScaledToRender(projectRelativeDataToScaledSpace(data)); + } + + /** + * Project a relative data vector from data space to rendering space. + * + * @param data relative vector in data space + * @return relative vector in rendering space + */ + @Override + public double[] projectRelativeDataToRenderSpace(double[] data) { + return projectRelativeScaledToRender(projectRelativeDataToScaledSpace(data)); + } + + /** + * Project a vector from scaled space to data space. + * + * @param <NV> Vector type + * @param v vector in scaled space + * @param factory Object factory + * @return vector in data space + */ + @Override + public <NV extends NumberVector> NV projectScaledToDataSpace(double[] v, NumberVector.Factory<NV> factory) { + final int dim = v.length; + double[] vec = new double[dim]; + for(int d = 0; d < dim; d++) { + vec[d] = scales[d].getUnscaled(v[d]); + } + return factory.newNumberVector(vec); + } + + /** + * Project a vector from rendering space to data space. + * + * @param <NV> Vector type + * @param v vector in rendering space + * @param prototype Object factory + * @return vector in data space + */ + @Override + public <NV extends NumberVector> NV projectRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype) { + final int dim = v.length; + double[] vec = projectRenderToScaled(v); + // Not calling {@link #projectScaledToDataSpace} to avoid extra copy of + // vector. + for(int d = 0; d < dim; d++) { + vec[d] = scales[d].getUnscaled(vec[d]); + } + return prototype.newNumberVector(vec); + } + + /** + * Project a relative vector from scaled space to data space. + * + * @param <NV> Vector type + * @param v relative vector in scaled space + * @param prototype Object factory + * @return relative vector in data space + */ + @Override + public <NV extends NumberVector> NV projectRelativeScaledToDataSpace(double[] v, NumberVector.Factory<NV> prototype) { + final int dim = v.length; + double[] vec = new double[dim]; + for(int d = 0; d < dim; d++) { + vec[d] = scales[d].getRelativeUnscaled(v[d]); + } + return prototype.newNumberVector(vec); + } + + /** + * Project a relative vector from rendering space to data space. + * + * @param <NV> Vector type + * @param v relative vector in rendering space + * @param prototype Object factory + * @return relative vector in data space + */ + @Override + public <NV extends NumberVector> NV projectRelativeRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype) { + final int dim = v.length; + double[] vec = projectRelativeRenderToScaled(v); + // Not calling {@link #projectScaledToDataSpace} to avoid extra copy of + // vector. + for(int d = 0; d < dim; d++) { + vec[d] = scales[d].getRelativeUnscaled(vec[d]); + } + return prototype.newNumberVector(vec); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractProjection.java new file mode 100644 index 00000000..bbcd1bcd --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractProjection.java @@ -0,0 +1,82 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; + +/** + * Abstract base projection class. + * + * @author Erich Schubert + */ +public abstract class AbstractProjection implements Projection { + /** + * Scales in data set + */ + final protected LinearScale[] scales; + + /** + * Projector used + */ + final private Projector p; + + /** + * Constructor. + * + * @param p Projector + * @param scales Scales to use + */ + public AbstractProjection(Projector p, LinearScale[] scales) { + super(); + this.p = p; + this.scales = scales; + } + + @Override + public int getInputDimensionality() { + return scales.length; + } + + /** + * Get the scales used, for rendering scales mostly. + * + * @param d Dimension + * @return Scale used + */ + @Override + public LinearScale getScale(int d) { + return scales[d]; + } + + @Override + public String getMenuName() { + return "Projection"; + } + + @Override + public Projector getProjector() { + return p; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractSimpleProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractSimpleProjection.java new file mode 100644 index 00000000..4b0715a9 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractSimpleProjection.java @@ -0,0 +1,110 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; + +/** + * Abstract base class for "simple" projections. + * + * Simple projections use the given scaling and dimension selection only. + * + * @author Erich Schubert + */ +public abstract class AbstractSimpleProjection extends AbstractFullProjection { + /** + * Constructor. + * + * @param p Projector + * @param scales Scales to use + */ + public AbstractSimpleProjection(Projector p, LinearScale[] scales) { + super(p, scales); + } + + @Override + public double[] projectScaledToRender(double[] v) { + v = rearrange(v); + VMath.minusEquals(v, .5); + v = flipSecondEquals(v); + VMath.timesEquals(v, SCALE); + return v; + } + + @Override + public double[] projectRenderToScaled(double[] v) { + v = VMath.times(v, INVSCALE); + v = flipSecondEquals(v); + VMath.plusEquals(v, .5); + v = dearrange(v); + return v; + } + + @Override + public double[] projectRelativeScaledToRender(double[] v) { + v = rearrange(v); + v = flipSecondEquals(v); + VMath.timesEquals(v, SCALE); + return v; + } + + @Override + public double[] projectRelativeRenderToScaled(double[] v) { + v = VMath.times(v, INVSCALE); + v = flipSecondEquals(v); + v = dearrange(v); + return v; + } + + /** + * Flip the y axis. + * + * @param v double[] + * @return modified v + */ + protected double[] flipSecondEquals(double[] v) { + if(v.length > 1) { + v[1] *= -1; + } + return v; + } + + /** + * Method to rearrange components. + * + * @param v double[] to rearrange + * @return rearranged copy + */ + protected abstract double[] rearrange(double[] v); + + /** + * Undo the rearrangement of components. + * + * @param v double[] to undo the rearrangement + * @return rearranged-undone copy + */ + protected abstract double[] dearrange(double[] v); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AffineProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AffineProjection.java new file mode 100644 index 00000000..a04c57fc --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AffineProjection.java @@ -0,0 +1,277 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Arrays; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.math.DoubleMinMax; +import de.lmu.ifi.dbs.elki.math.linearalgebra.AffineTransformation; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.utilities.BitsUtil; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; + +/** + * Affine projections are the most general class. They are initialized by an + * arbitrary affine transformation matrix, and can thus represent any rotation + * and scaling, even simple perspective projections. + * + * However, this comes at the cost of a matrix multiplication. + * + * @author Erich Schubert + */ +public class AffineProjection extends AbstractFullProjection implements Projection2D { + /** + * Affine transformation used in projection + */ + private AffineTransformation proj; + + /** + * Viewport (cache) + */ + private CanvasSize viewport = null; + + /** + * Constructor with a given database and axes. + * + * @param p Projector + * @param scales Scales to use + * @param proj Projection to use + */ + public AffineProjection(Projector p, LinearScale[] scales, AffineTransformation proj) { + super(p, scales); + this.proj = proj; + } + + /** + * Project a vector from scaled space to rendering space. + * + * @param v vector in scaled space + * @return vector in rendering space + */ + @Override + public double[] projectScaledToRender(double[] v) { + return proj.apply(v); + } + + /** + * Project a vector from rendering space to scaled space. + * + * @param v vector in rendering space + * @return vector in scaled space + */ + @Override + public double[] projectRenderToScaled(double[] v) { + return proj.applyInverse(v); + } + + /** + * Project a relative vector from scaled space to rendering space. + * + * @param v relative vector in scaled space + * @return relative vector in rendering space + */ + @Override + public double[] projectRelativeScaledToRender(double[] v) { + return proj.applyRelative(v); + } + + /** + * Project a relative vector from rendering space to scaled space. + * + * @param v relative vector in rendering space + * @return relative vector in scaled space + */ + @Override + public double[] projectRelativeRenderToScaled(double[] v) { + return proj.applyRelativeInverse(v); + } + + @Override + public CanvasSize estimateViewport() { + if(viewport == null) { + final int dim = proj.getDimensionality(); + DoubleMinMax minmaxx = new DoubleMinMax(); + DoubleMinMax minmaxy = new DoubleMinMax(); + + // Origin + final double[] vec = new double[dim]; + double[] orig = projectScaledToRender(vec); + minmaxx.put(orig[0]); + minmaxy.put(orig[1]); + // Diagonal point + Arrays.fill(vec, 1.); + double[] diag = projectScaledToRender(vec); + minmaxx.put(diag[0]); + minmaxy.put(diag[1]); + // Axis end points + for(int d = 0; d < dim; d++) { + Arrays.fill(vec, 0.); + vec[d] = 1.; + double[] ax = projectScaledToRender(vec); + minmaxx.put(ax[0]); + minmaxy.put(ax[1]); + } + viewport = new CanvasSize(minmaxx.getMin(), minmaxx.getMax(), minmaxy.getMin(), minmaxy.getMax()); + } + return viewport; + } + + /** + * Compute an transformation matrix to show only axis ax1 and ax2. + * + * @param dim Dimensionality + * @param ax1 First axis + * @param ax2 Second axis + * @return transformation matrix + */ + public static AffineTransformation axisProjection(int dim, int ax1, int ax2) { + // setup a projection to get the data into the interval -1:+1 in each + // dimension with the intended-to-see dimensions first. + AffineTransformation proj = AffineTransformation.reorderAxesTransformation(dim, new int[] { ax1, ax2 }); + // Assuming that the data was normalized on [0:1], center it: + double[] trans = new double[dim]; + for(int i = 0; i < dim; i++) { + trans[i] = -.5; + } + proj.addTranslation(trans); + // mirror on the y axis, since the SVG coordinate system is screen + // coordinates (y = down) and not mathematical coordinates (y = up) + proj.addAxisReflection(2); + // scale it up + proj.addScaling(SCALE); + + return proj; + } + + @Override + public double[] fastProjectDataToRenderSpace(double[] data) { + return fastProjectScaledToRenderSpace(fastProjectDataToScaledSpace(data)); + } + + @Override + public double[] fastProjectDataToRenderSpace(NumberVector data) { + return fastProjectScaledToRenderSpace(fastProjectDataToScaledSpace(data)); + } + + @Override + public double[] fastProjectDataToScaledSpace(double[] data) { + return projectDataToScaledSpace(data); + } + + @Override + public double[] fastProjectDataToScaledSpace(NumberVector data) { + return projectDataToScaledSpace(data); + } + + @Override + public double[] fastProjectScaledToRenderSpace(double[] vr) { + double x = 0.0; + double y = 0.0; + double s = 0.0; + + final double[][] matrix = proj.getTransformation().getArrayRef(); + final double[] colx = matrix[0]; + final double[] coly = matrix[1]; + final double[] cols = matrix[vr.length]; + assert (colx.length == coly.length && colx.length == cols.length && cols.length == vr.length + 1); + + for(int k = 0; k < vr.length; k++) { + x += colx[k] * vr[k]; + y += coly[k] * vr[k]; + s += cols[k] * vr[k]; + } + // add homogene component: + x += colx[vr.length]; + y += coly[vr.length]; + s += cols[vr.length]; + // Note: we may have NaN values here. + // assert (s > 0.0 || s < 0.0); + return new double[] { x / s, y / s }; + } + + @Override + public double[] fastProjectRelativeDataToRenderSpace(double[] data) { + return fastProjectRelativeScaledToRenderSpace(projectRelativeDataToScaledSpace(data)); + } + + @Override + public double[] fastProjectRelativeDataToRenderSpace(NumberVector data) { + // FIXME: implement with less objects? + return fastProjectRelativeScaledToRenderSpace(projectRelativeDataToScaledSpace(data)); + } + + @Override + public double[] fastProjectRelativeScaledToRenderSpace(double[] vr) { + double x = 0.0; + double y = 0.0; + + final double[][] matrix = proj.getTransformation().getArrayRef(); + final double[] colx = matrix[0]; + final double[] coly = matrix[1]; + assert (colx.length == coly.length); + + for(int k = 0; k < vr.length; k++) { + x += colx[k] * vr[k]; + y += coly[k] * vr[k]; + } + return new double[] { x, y }; + } + + @Override + public double[] fastProjectRenderToDataSpace(double x, double y) { + double[] ret = fastProjectRenderToScaledSpace(x, y); + for(int d = 0; d < scales.length; d++) { + ret[d] = scales[d].getUnscaled(ret[d]); + } + return ret; + } + + @Override + public double[] fastProjectRenderToScaledSpace(double x, double y) { + double[] c = new double[scales.length]; + c[0] = x; + c[1] = y; + Arrays.fill(c, 2, scales.length, 0.5); + return projectRenderToScaled(c); + } + + @Override + public long[] getVisibleDimensions2D() { + final int dim = proj.getDimensionality(); + long[] actDim = BitsUtil.zero(dim); + double[] vScale = new double[dim]; + for(int d = 0; d < dim; d++) { + Arrays.fill(vScale, 0); + vScale[d] = 1; + double[] vRender = fastProjectScaledToRenderSpace(vScale); + + // TODO: Can't we do this by inspecting the projection matrix directly? + if(vRender[0] > 0.0 || vRender[0] < 0.0 || vRender[1] != 0) { + BitsUtil.setI(actDim, d); + } + } + return actDim; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/CanvasSize.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/CanvasSize.java new file mode 100644 index 00000000..b915ed07 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/CanvasSize.java @@ -0,0 +1,139 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +/** + * Size of a canvas. A 2D bounding rectangle. + * + * @author Erich Schubert + */ +public class CanvasSize { + /** + * Minimum X + */ + public final double minx; + + /** + * Maximum X + */ + public final double maxx; + + /** + * Minimum Y + */ + public final double miny; + + /** + * Maximum Y + */ + public final double maxy; + + /** + * Constructor. + * + * @param minx Minimum X + * @param maxx Maximum X + * @param miny Minimum Y + * @param maxy Maximum Y + */ + public CanvasSize(double minx, double maxx, double miny, double maxy) { + super(); + this.minx = minx; + this.maxx = maxx; + this.miny = miny; + this.maxy = maxy; + } + + /** + * @return the mininum X + */ + public double getMinX() { + return minx; + } + + /** + * @return the maximum X + */ + public double getMaxX() { + return maxx; + } + + /** + * @return the minimum Y + */ + public double getMinY() { + return miny; + } + + /** + * @return the maximum Y + */ + public double getMaxY() { + return maxy; + } + + /** + * @return the length on X + */ + public double getDiffX() { + return maxx - minx; + } + + /** + * @return the length on Y + */ + public double getDiffY() { + return maxy - miny; + } + + /** + * Continue a line along a given direction to the margin. + * + * @param origin Origin point + * @param delta Direction vector + * @return scaling factor for delta vector + */ + public double continueToMargin(double[] origin, double[] delta) { + assert (delta.length == 2 && origin.length == 2); + double factor = Double.POSITIVE_INFINITY; + if(delta[0] > 0) { + factor = Math.min(factor, (maxx - origin[0]) / delta[0]); + } + else if(delta[0] < 0) { + factor = Math.min(factor, (origin[0] - minx) / -delta[0]); + } + if(delta[1] > 0) { + factor = Math.min(factor, (maxy - origin[1]) / delta[1]); + } + else if(delta[1] < 0) { + factor = Math.min(factor, (origin[1] - miny) / -delta[1]); + } + return factor; + } + + @Override + public String toString() { + return "CanvasSize[x=" + minx + ":" + maxx + ", y=" + miny + ":" + maxy + "]"; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/FullProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/FullProjection.java new file mode 100644 index 00000000..553d8faa --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/FullProjection.java @@ -0,0 +1,174 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ +import de.lmu.ifi.dbs.elki.data.NumberVector; + +/** + * Full vector space projections. + * + * These rather portable projections offer a large choice of functions, at the + * cost of often being a bit slower than the low level functions. + * + * Note: this interface and methods may be removed, unless there is a clear use + * case for them as opposed to always using the low-level fast projections. + * + * @author Erich Schubert + */ +public interface FullProjection extends Projection { + /** + * Project a vector from scaled space to rendering space. + * + * @param v vector in scaled space + * @return vector in rendering space + */ + double[] projectScaledToRender(double[] v); + + /** + * Project a vector from rendering space to scaled space. + * + * @param v vector in rendering space + * @return vector in scaled space + */ + double[] projectRenderToScaled(double[] v); + + /** + * Project a relative vector from scaled space to rendering space. + * + * @param v relative vector in scaled space + * @return relative vector in rendering space + */ + double[] projectRelativeScaledToRender(double[] v); + + /** + * Project a relative vector from rendering space to scaled space. + * + * @param v relative vector in rendering space + * @return relative vector in scaled space + */ + double[] projectRelativeRenderToScaled(double[] v); + + /** + * Project a data vector from data space to scaled space. + * + * @param data vector in data space + * @return vector in scaled space + */ + double[] projectDataToScaledSpace(NumberVector data); + + /** + * Project a data vector from data space to scaled space. + * + * @param data vector in data space + * @return vector in scaled space + */ + double[] projectDataToScaledSpace(double[] data); + + /** + * Project a relative data vector from data space to scaled space. + * + * @param data relative vector in data space + * @return relative vector in scaled space + */ + double[] projectRelativeDataToScaledSpace(NumberVector data); + + /** + * Project a relative data vector from data space to scaled space. + * + * @param data relative vector in data space + * @return relative vector in scaled space + */ + double[] projectRelativeDataToScaledSpace(double[] data); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + double[] projectDataToRenderSpace(NumberVector data); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + double[] projectDataToRenderSpace(double[] data); + + /** + * Project a vector from scaled space to data space. + * + * @param <NV> double[] type + * @param v vector in scaled space + * @param factory Object factory + * @return vector in data space + */ + <NV extends NumberVector> NV projectScaledToDataSpace(double[] v, NumberVector.Factory<NV> factory); + + /** + * Project a vector from rendering space to data space. + * + * @param <NV> double[] type + * @param v vector in rendering space + * @param prototype Object factory + * @return vector in data space + */ + <NV extends NumberVector> NV projectRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype); + + /** + * Project a relative data vector from data space to rendering space. + * + * @param data relative vector in data space + * @return relative vector in rendering space + */ + double[] projectRelativeDataToRenderSpace(NumberVector data); + + /** + * Project a relative data vector from data space to rendering space. + * + * @param data relative vector in data space + * @return relative vector in rendering space + */ + double[] projectRelativeDataToRenderSpace(double[] data); + + /** + * Project a relative vector from scaled space to data space. + * + * @param <NV> double[] type + * @param v relative vector in scaled space + * @param prototype Object factory + * @return relative vector in data space + */ + <NV extends NumberVector> NV projectRelativeScaledToDataSpace(double[] v, NumberVector.Factory<NV> prototype); + + /** + * Project a relative vector from rendering space to data space. + * + * @param <NV> double[] type + * @param v relative vector in rendering space + * @param prototype Object factory + * @return relative vector in data space + */ + <NV extends NumberVector> NV projectRelativeRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/OPTICSProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/OPTICSProjection.java new file mode 100644 index 00000000..b06d0a6d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/OPTICSProjection.java @@ -0,0 +1,93 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot; +import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; + +/** + * OPTICS projection. This is not really needed, but a quick hack to have more + * consistency in the visualizer API. + * + * @author Erich Schubert + */ +public class OPTICSProjection implements Projection { + /** + * The projector we were generated from. + */ + OPTICSProjector projector; + + /** + * Constructor. + * + * @param opticsProjector OPTICS projector + */ + public OPTICSProjection(OPTICSProjector opticsProjector) { + super(); + this.projector = opticsProjector; + } + + @Override + public String getMenuName() { + return "OPTICS Plot Projection"; + } + + @Override + public int getInputDimensionality() { + return -1; + } + + @Override + public LinearScale getScale(int d) { + return null; + } + + /** + * Get or produce the actual OPTICS plot. + * + * @param context Context to use + * @return Plot + */ + public OPTICSPlot getOPTICSPlot(VisualizerContext context) { + return projector.getOPTICSPlot(context); + } + + /** + * Get the OPTICS cluster order. + * + * @return Cluster oder result. + */ + public ClusterOrder getResult() { + return projector.getResult(); + } + + @Override + public Projector getProjector() { + return projector; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection.java new file mode 100644 index 00000000..1a501608 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection.java @@ -0,0 +1,76 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; + +/** + * Base interface used for projections in the ELKI visualizers. + * + * There are specialized interfaces for 1D and 2D that only compute the + * projections in the required dimensions! + * + * @author Erich Schubert + * + * @apiviz.landmark + * + * @apiviz.composedOf LinearScale + */ +public interface Projection extends VisualizationItem { + /** + * Scaling constant. Keep in sync with + * {@link de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary#SCALE}. + */ + public static final double SCALE = StyleLibrary.SCALE; + + /** + * Inverse scaling constant. + */ + public static final double INVSCALE = 1. / SCALE; + + /** + * Get the input dimensionality of the projection. + * + * @return Input dimensionality + */ + public int getInputDimensionality(); + + /** + * Get the scale class for a particular dimension. + * + * @param d Dimension + * @return Scale class + */ + public LinearScale getScale(int d); + + /** + * Projector used for generating this projection. + * + * @return Projector + */ + public Projector getProjector(); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection1D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection1D.java new file mode 100644 index 00000000..a5daf0d3 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection1D.java @@ -0,0 +1,84 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; + +/** + * Interface for projections that have a specialization to only compute the + * first component. + * + * @author Erich Schubert + * + * @apiviz.landmark + */ +public interface Projection1D extends Projection { + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double fastProjectDataToRenderSpace(double[] data); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double fastProjectDataToRenderSpace(NumberVector data); + + /** + * Project a vector from scaled space to rendering space. + * + * @param v vector in scaled space + * @return vector in rendering space + */ + public double fastProjectScaledToRender(double[] v); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double fastProjectRelativeDataToRenderSpace(double[] data); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double fastProjectRelativeDataToRenderSpace(NumberVector data); + + /** + * Project a vector from scaled space to rendering space. + * + * @param v vector in scaled space + * @return vector in rendering space + */ + public double fastProjectRelativeScaledToRender(double[] v); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection2D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection2D.java new file mode 100644 index 00000000..6f9f17b0 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection2D.java @@ -0,0 +1,146 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; + +/** + * Projections that have specialized methods to only compute the first two + * dimensions of the projection. + * + * @author Erich Schubert + * + * @apiviz.landmark + * + * @apiviz.has CanvasSize + */ +public interface Projection2D extends Projection { + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double[] fastProjectDataToRenderSpace(double[] data); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double[] fastProjectDataToRenderSpace(NumberVector data); + + /** + * Project a data vector from data space to scaled space. + * + * @param data vector in data space + * @return vector in scaled space + */ + public double[] fastProjectDataToScaledSpace(double[] data); + + /** + * Project a data vector from data space to scaled space. + * + * @param data vector in data space + * @return vector in scaled space + */ + public double[] fastProjectDataToScaledSpace(NumberVector data); + + /** + * Project a vector from scaled space to rendering space. + * + * @param v vector in scaled space + * @return vector in rendering space + */ + public double[] fastProjectScaledToRenderSpace(double[] v); + + /** + * Project a data vector from rendering space to data space. + * + * @param x X coordinate + * @param y Y coordinate + * @return vector in data space + */ + public double[] fastProjectRenderToDataSpace(double x, double y); + + /** + * Project a data vector from rendering space to data space. + * + * @param data vector in rendering space + * @param prototype Prototype to create vector from + * @return vector in data space + */ + // public <V extends NumberVector> V fastProjectRenderToDataSpace(double[] + // data, V prototype); + + /** + * Project a vector from rendering space to scaled space. + * + * @param x X coordinate + * @param y Y coordinate + * @return vector in scaled space + */ + public double[] fastProjectRenderToScaledSpace(double x, double y); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double[] fastProjectRelativeDataToRenderSpace(double[] data); + + /** + * Project a data vector from data space to rendering space. + * + * @param data vector in data space + * @return vector in rendering space + */ + public double[] fastProjectRelativeDataToRenderSpace(NumberVector data); + + /** + * Project a vector from scaled space to rendering space. + * + * @param v vector in scaled space + * @return vector in rendering space + */ + public double[] fastProjectRelativeScaledToRenderSpace(double[] v); + + // FIXME: add missing relative projection functions + + /** + * Estimate the viewport requirements + * + * @return Canvas size obtained from projecting scale endpoints + */ + public CanvasSize estimateViewport(); + + /** + * Get a bit set of dimensions that are visible. + * + * @return Bit set, first dimension is bit 0. + */ + public long[] getVisibleDimensions2D(); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/ProjectionParallel.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/ProjectionParallel.java new file mode 100644 index 00000000..435a2146 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/ProjectionParallel.java @@ -0,0 +1,196 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; + +/** + * Projection to parallel coordinates that allows reordering and inversion of + * axes. + * + * Note: when using this projection, pay attention to the two different schemes + * of dimension numbering: by input dimension and by axis position. + * + * @author Robert Rödler + * @author Erich Schubert + */ +public interface ProjectionParallel extends Projection { + /** + * Get inversion flag of axis. + * + * @param axis Axis (reordered) position + * @return Inversion flag + */ + public boolean isAxisInverted(int axis); + + /** + * Set inversion flag of axis. + * + * @param axis Axis (reordered) position + * @param bool Value of inversion flag + */ + public void setAxisInverted(int axis, boolean bool); + + /** + * Toggle inverted flag of axis. + * + * @param axis Axis (reordered) position + */ + public void toggleAxisInverted(int axis); + + /** + * Get inversion flag of dimension. + * + * @param truedim Dimension in original numbering + * @return Inversion flag + */ + public boolean isDimInverted(int truedim); + + /** + * Set inversion flag of a dimension. + * + * @param truedim Dimension in original numbering + * @param bool Value of inversion flag + */ + public void setDimInverted(int truedim, boolean bool); + + /** + * Toggle inverted flag of dimension. + * + * @param truedim Dimension in original numbering + */ + public void toggleDimInverted(int truedim); + + /** + * Get scale for the given axis + * + * @param axis Axis (reordered) position + * @return Axis scale + */ + public LinearScale getAxisScale(int axis); + + /** + * Test whether the current axis is visible + * + * @param axis Axis (reordered) position + * @return Visibility of axis + */ + public boolean isAxisVisible(int axis); + + /** + * Set the visibility of the axis. + * + * @param axis Axis number + * @param vis Visibility status + */ + public void setAxisVisible(int axis, boolean vis); + + /** + * Toggle visibility of the axis. + * + * @param axis Axis number + */ + public void toggleAxisVisible(int axis); + + /** + * Get the number of visible dimension. + * + * @return Number of visible dimensions + */ + public int getVisibleDimensions(); + + /** + * Exchange axes A and B + * @param a First axis + * @param b Second axis + */ + public void swapAxes(int a, int b); + + /** + * shift a dimension to another position + * + * @param axis axis to shift + * @param rn new position + */ + public void moveAxis(int axis, int rn); + + /** + * Get the dimension for the given axis number + * + * @param axis Axis number + * @return Dimension + */ + public int getDimForAxis(int axis); + + /** + * Get the dimension for the given visible axis + * + * @param axis Axis number (visible axes only) + * @return Dimension + */ + public int getDimForVisibleAxis(int axis); + + /** + * Fast project a vector from data to render space + * + * @param v Input vector + * @return Vector with reordering, inversions and scales applied. + */ + public double[] fastProjectDataToRenderSpace(double[] v); + + /** + * Fast project a vector from data to render space + * + * @param v Input vector + * @return Vector with reordering, inversions and scales applied. + */ + public double[] fastProjectDataToRenderSpace(NumberVector v); + + /** + * Project the value of a single axis to its display value + * + * @param value Input value + * @param axis Axis to use for scaling and inversion + * @return Transformed value + */ + public double fastProjectDataToRenderSpace(double value, int axis); + + /** + * Project a display value back to the original data space + * + * @param value transformed value + * @param axis Axis to use for scaling and inversion + * @return Original value + */ + public double fastProjectRenderToDataSpace(double value, int axis); + + /** + * Find the axis assigned to the given dimension. + * + * @param truedim Dimension + * @return Axis number + */ + public int getAxisForDim(int truedim); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple1D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple1D.java new file mode 100644 index 00000000..b4560152 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple1D.java @@ -0,0 +1,114 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; + +/** + * Dimension-selecting 1D projection. + * + * @author Erich Schubert + */ +public class Simple1D extends AbstractSimpleProjection implements Projection1D { + /** + * Our dimension, starting with 0 + */ + final int dnum; + + /** + * Simple 1D projection using scaling only. + * + * @param p Projector + * @param scales Scales to use + * @param dnum Dimension (starting at 0) + */ + public Simple1D(Projector p, LinearScale[] scales, int dnum) { + super(p, scales); + this.dnum = dnum; + } + + @Override + public double fastProjectDataToRenderSpace(double[] data) { + return (scales[dnum].getScaled(data[dnum]) - 0.5) * SCALE; + } + + @Override + public double fastProjectDataToRenderSpace(NumberVector data) { + return (scales[dnum].getScaled(data.doubleValue(dnum)) - 0.5) * SCALE; + } + + @Override + public double fastProjectScaledToRender(double[] v) { + return (v[dnum] - 0.5) * SCALE; + } + + @Override + public double fastProjectRelativeDataToRenderSpace(double[] data) { + return (scales[dnum].getScaled(data[dnum]) - 0.5) * SCALE; + } + + @Override + public double fastProjectRelativeDataToRenderSpace(NumberVector data) { + return (data.doubleValue(dnum) - 0.5) * SCALE; + } + + @Override + public double fastProjectRelativeScaledToRender(double[] v) { + return v[dnum] * SCALE; + } + + @Override + protected double[] rearrange(double[] v) { + final double[] r = new double[v.length]; + r[0] = v[dnum]; + if(dnum > 0) { + System.arraycopy(v, 0, r, 1, dnum); + } + if(dnum + 1 < v.length) { + System.arraycopy(v, dnum + 1, r, dnum + 1, v.length - (dnum + 1)); + } + return r; + } + + @Override + protected double[] dearrange(double[] v) { + final double[] r = new double[v.length]; + if(dnum > 0) { + System.arraycopy(v, 1, r, 0, dnum); + } + r[dnum] = v[0]; + if(dnum + 1 < v.length) { + System.arraycopy(v, dnum + 1, r, dnum + 1, v.length - (dnum + 1)); + } + return r; + } + + + @Override + public String getMenuName() { + return "Axis"; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple2D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple2D.java new file mode 100644 index 00000000..3dff7c3b --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple2D.java @@ -0,0 +1,205 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.utilities.BitsUtil; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; + +/** + * Dimension-selecting 2D projection. + * + * @author Erich Schubert + */ +public class Simple2D extends AbstractSimpleProjection implements Projection2D { + /** + * Dimensions for fast projection mode. + */ + private int dim1; + + /** + * Dimensions for fast projection mode. + */ + private int dim2; + + /** + * Constructor with a given database and axes. + * + * @param p Projector + * @param scales Scales to use + * @param ax1 First axis + * @param ax2 Second axis + */ + public Simple2D(Projector p, LinearScale[] scales, int ax1, int ax2) { + super(p, scales); + this.dim1 = ax1; + this.dim2 = ax2; + } + + @Override + public double[] fastProjectDataToRenderSpace(double[] data) { + double x = (scales[dim1].getScaled(data[dim1]) - 0.5) * SCALE; + double y = (scales[dim2].getScaled(data[dim2]) - 0.5) * -SCALE; + return new double[] { x, y }; + } + + @Override + public double[] fastProjectDataToRenderSpace(NumberVector data) { + double x = (scales[dim1].getScaled(data.doubleValue(dim1)) - 0.5) * SCALE; + double y = (scales[dim2].getScaled(data.doubleValue(dim2)) - 0.5) * -SCALE; + return new double[] { x, y }; + } + + @Override + public double[] fastProjectDataToScaledSpace(double[] data) { + final int dim = data.length; + double[] ds = new double[dim]; + for(int d = 0; d < dim; d++) { + ds[d] = scales[d].getScaled(data[d]); + } + return ds; + } + + @Override + public double[] fastProjectDataToScaledSpace(NumberVector data) { + final int dim = data.getDimensionality(); + double[] ds = new double[dim]; + for(int d = 0; d < dim; d++) { + ds[d] = scales[d].getScaled(data.doubleValue(d)); + } + return ds; + } + + @Override + public double[] fastProjectScaledToRenderSpace(double[] v) { + double x = (v[dim1] - 0.5) * SCALE; + double y = (v[dim2] - 0.5) * -SCALE; + return new double[] { x, y }; + } + + @Override + public double[] fastProjectRenderToDataSpace(double x, double y) { + double[] ret = new double[scales.length]; + for(int d = 0; d < scales.length; d++) { + ret[d] = // + (d == dim1) ? scales[d].getUnscaled((x * INVSCALE) + 0.5) : // + (d == dim2) ? scales[d].getUnscaled((y * -INVSCALE) + 0.5) : // + scales[d].getUnscaled(0.5); + } + return ret; + } + + @Override + public double[] fastProjectRenderToScaledSpace(double x, double y) { + double[] ret = new double[scales.length]; + for(int d = 0; d < scales.length; d++) { + ret[d] = // + (d == dim1) ? (x * INVSCALE) + 0.5 : // + (d == dim2) ? (y * -INVSCALE) + 0.5 : // + 0.5; + } + return ret; + } + + @Override + public double[] fastProjectRelativeDataToRenderSpace(double[] data) { + double x = scales[dim1].getRelativeScaled(data[dim1]) * SCALE; + double y = scales[dim2].getRelativeScaled(data[dim2]) * -SCALE; + return new double[] { x, y }; + } + + @Override + public double[] fastProjectRelativeDataToRenderSpace(NumberVector data) { + double x = scales[dim1].getRelativeScaled(data.doubleValue(dim1)) * SCALE; + double y = scales[dim2].getRelativeScaled(data.doubleValue(dim2)) * -SCALE; + return new double[] { x, y }; + } + + @Override + public double[] fastProjectRelativeScaledToRenderSpace(double[] vr) { + double x = vr[dim1] * SCALE; + double y = vr[dim2] * -SCALE; + return new double[] { x, y }; + } + + @Override + public long[] getVisibleDimensions2D() { + long[] actDim = new long[super.scales.length]; + BitsUtil.setI(actDim, dim1); + BitsUtil.setI(actDim, dim2); + return actDim; + } + + @Override + public CanvasSize estimateViewport() { + return new CanvasSize(-SCALE * .5, SCALE * .5, -SCALE * .5, SCALE * .5); + } + + @Override + protected double[] rearrange(double[] v) { + final double[] r = new double[v.length]; + r[0] = v[dim1]; + r[1] = v[dim2]; + final int ldim = Math.min(dim1, dim2); + final int hdim = Math.max(dim1, dim2); + if(ldim > 0) { + System.arraycopy(v, 0, r, 2, ldim); + } + if(hdim - ldim > 1) { + System.arraycopy(v, ldim + 1, r, ldim + 2, hdim - (ldim + 1)); + } + if(hdim + 1 < v.length) { + System.arraycopy(v, hdim + 1, r, hdim + 1, v.length - (hdim + 1)); + } + return r; + } + + @Override + protected double[] dearrange(double[] v) { + final double[] r = new double[v.length]; + r[dim1] = v[0]; + r[dim2] = v[1]; + // copy remainder + final int ldim = Math.min(dim1, dim2); + final int hdim = Math.max(dim1, dim2); + if(ldim > 0) { + System.arraycopy(v, 2, r, 0, ldim); + } + // ldim = s[0 or 1] + if(hdim - ldim > 1) { + System.arraycopy(v, ldim + 2, r, ldim + 1, hdim - (ldim + 1)); + } + // hdim = s[0 or 1] + if(hdim + 1 < v.length) { + System.arraycopy(v, hdim + 1, r, hdim + 1, v.length - (hdim + 1)); + } + return r; + } + + @Override + public String getMenuName() { + return "Scatterplot"; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/SimpleParallel.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/SimpleParallel.java new file mode 100644 index 00000000..e25d4088 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/SimpleParallel.java @@ -0,0 +1,293 @@ +package de.lmu.ifi.dbs.elki.visualization.projections; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; + +/** + * Simple parallel projection + * + * Scaled space: reordered, scaled and inverted. Lower dimensionality! [0:1] + * Render space: not used here; no recentering needed. + * + * @author Robert Rödler + * @author Erich Schubert + */ +public class SimpleParallel implements ProjectionParallel { + /** + * Number of visible dimensions + */ + int visDims; + + /** + * Flags for the dimensions + */ + byte[] flags; + + /** + * Ordering of dimensions + */ + int[] dimOrder; + + /** + * Scales + */ + private LinearScale[] scales; + + /** + * Projector + */ + private Projector p; + + /** + * Flag for visibility + */ + final static byte FLAG_HIDDEN = 1; + + /** + * Flag for inverted dimensions + * + * TODO: handle inversions via scales? + */ + final static byte FLAG_INVERTED = 2; + + /** + * Constructor. + * + * @param p Projector + * @param scales Scales to use + */ + public SimpleParallel(Projector p, LinearScale[] scales) { + super(); + this.p = p; + this.scales = scales; + visDims = scales.length; + flags = new byte[scales.length]; + dimOrder = new int[scales.length]; + for(int i = 0; i < dimOrder.length; i++) { + dimOrder[i] = i; + } + } + + @Override + public LinearScale getScale(int dim) { + return scales[dim]; + } + + @Override + public boolean isAxisInverted(int axis) { + return isDimInverted(dimOrder[axis]); + } + + @Override + public void setAxisInverted(int axis, boolean bool) { + setDimInverted(dimOrder[axis], bool); + } + + @Override + public void toggleAxisInverted(int axis) { + toggleDimInverted(dimOrder[axis]); + } + + @Override + public boolean isDimInverted(int truedim) { + return (flags[truedim] & FLAG_INVERTED) == FLAG_INVERTED; + } + + @Override + public void setDimInverted(int truedim, boolean bool) { + if(bool) { + flags[truedim] |= FLAG_INVERTED; + } + else { + flags[truedim] &= ~FLAG_INVERTED; + } + } + + @Override + public void toggleDimInverted(int truedim) { + flags[truedim] ^= FLAG_INVERTED; + } + + @Override + public LinearScale getAxisScale(int axis) { + return scales[dimOrder[axis]]; + } + + protected boolean isDimHidden(int truedim) { + return (flags[truedim] & FLAG_HIDDEN) == FLAG_HIDDEN; + } + + @Override + public boolean isAxisVisible(int dim) { + return !isDimHidden(dimOrder[dim]); + } + + @Override + public void setAxisVisible(int dim, boolean vis) { + boolean prev = isAxisVisible(dim); + if(prev == vis) { + return; + } + if(vis) { + flags[dimOrder[dim]] &= ~FLAG_HIDDEN; + visDims++; + } + else { + flags[dimOrder[dim]] |= FLAG_HIDDEN; + visDims--; + } + } + + @Override + public void toggleAxisVisible(int dim) { + boolean prev = isAxisVisible(dim); + if(!prev) { + flags[dimOrder[dim]] &= ~FLAG_HIDDEN; + visDims++; + } + else { + flags[dimOrder[dim]] |= FLAG_HIDDEN; + visDims--; + } + } + + @Override + public int getVisibleDimensions() { + return visDims; + } + + @Override + public int getDimForAxis(int pos) { + return dimOrder[pos]; + } + + @Override + public int getDimForVisibleAxis(int pos) { + for(int i = 0; i < scales.length; i++) { + if(isDimHidden(dimOrder[i])) { + continue; + } + if(pos == 0) { + return dimOrder[i]; + } + pos--; + } + return -1; + } + + @Override + public void swapAxes(int a, int b) { + int temp = dimOrder[a]; + dimOrder[a] = dimOrder[b]; + dimOrder[b] = temp; + } + + @Override + public void moveAxis(int src, int dest) { + if(src > dest) { + int temp = dimOrder[src]; + System.arraycopy(dimOrder, dest, dimOrder, dest + 1, src - dest); + dimOrder[dest] = temp; + } + else if(src < dest) { + int temp = dimOrder[src]; + System.arraycopy(dimOrder, src + 1, dimOrder, src, dest - src); + dimOrder[dest - 1] = temp; + } + } + + @Override + public double[] fastProjectDataToRenderSpace(NumberVector data) { + double[] v = new double[visDims]; + for(int j = 0, o = 0; j < scales.length; j++) { + if(isDimHidden(j)) { + continue; + } + int i = dimOrder[j]; + double w = scales[i].getScaled(data.doubleValue(i)); + w = isDimInverted(i) ? w : 1 - w; + v[o++] = w * StyleLibrary.SCALE; + } + return v; + } + + @Override + public double[] fastProjectDataToRenderSpace(double[] data) { + double[] v = new double[visDims]; + for(int j = 0, o = 0; j < scales.length; j++) { + if(isDimHidden(j)) { + continue; + } + int i = dimOrder[j]; + double w = scales[i].getScaled(data[i]); + w = isDimInverted(i) ? w : 1 - w; + v[o++] = w * StyleLibrary.SCALE; + } + return v; + } + + @Override + public double fastProjectRenderToDataSpace(double v, int projdim) { + int truedim = dimOrder[projdim]; + v /= StyleLibrary.SCALE; + v = isDimInverted(truedim) ? v : 1 - v; + return scales[truedim].getUnscaled(v); + } + + @Override + public double fastProjectDataToRenderSpace(double value, int dim) { + double temp = scales[dimOrder[dim]].getScaled(value); + temp *= StyleLibrary.SCALE; + return isAxisInverted(dimOrder[dim]) ? 1 - temp : temp; + } + + @Override + public int getAxisForDim(int truedim) { + for(int i = 0; i < dimOrder.length; i++) { + if(dimOrder[i] == truedim) { + return i; + } + } + return -1; + } + + @Override + public int getInputDimensionality() { + return scales.length; + } + + @Override + public String getMenuName() { + return "Parallel Coordinates"; + } + + @Override + public Projector getProjector() { + return p; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/package-info.java new file mode 100755 index 00000000..b903fa37 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualization projections</p> + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.projections;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramFactory.java new file mode 100644 index 00000000..c7843576 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramFactory.java @@ -0,0 +1,116 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; + +/** + * Produce one-dimensional projections. + * + * @author Erich Schubert + * + * @apiviz.has HistogramProjector + */ +public class HistogramFactory implements ProjectorFactory { + /** + * Maximum dimensionality. + */ + private int maxdim = ScatterPlotFactory.MAX_DIMENSIONS_DEFAULT; + + /** + * Constructor. + * + * @param maxdim Maximum dimensionality + */ + public HistogramFactory(int maxdim) { + super(); + this.maxdim = maxdim; + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<Relation<?>> it1 = VisualizationTree.filterResults(context, start, Relation.class); + candidate: for(; it1.valid(); it1.advance()) { + Relation<?> rel = it1.get(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + // Do not enable nested relations by default: + Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<>(context.getHierarchy().iterAncestors(rel), Relation.class); + for(; it2.valid(); it2.advance()) { + // Parent relation + final Relation<?> rel2 = (Relation<?>) it2.get(); + if(TypeUtil.SPATIAL_OBJECT.isAssignableFromType(rel2.getDataTypeInformation())) { + continue candidate; + } + // TODO: add Actions instead. + } + @SuppressWarnings("unchecked") + Relation<NumberVector> vrel = (Relation<NumberVector>) rel; + final int dim = RelationUtil.dimensionality(vrel); + HistogramProjector<NumberVector> proj = new HistogramProjector<>(vrel, Math.min(dim, maxdim)); + context.addVis(vrel, proj); + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Stores the maximum number of dimensions to show. + */ + private int maxdim = ScatterPlotFactory.MAX_DIMENSIONS_DEFAULT; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + IntParameter maxdimP = new IntParameter(ScatterPlotFactory.Parameterizer.MAXDIM_ID, ScatterPlotFactory.MAX_DIMENSIONS_DEFAULT); + maxdimP.addConstraint(CommonConstraints.GREATER_EQUAL_ZERO_INT); + if(config.grab(maxdimP)) { + maxdim = maxdimP.intValue(); + } + } + + @Override + protected HistogramFactory makeInstance() { + return new HistogramFactory(maxdim); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramProjector.java new file mode 100644 index 00000000..d46265e7 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramProjector.java @@ -0,0 +1,122 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.ScalesResult; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection1D; +import de.lmu.ifi.dbs.elki.visualization.projections.Simple1D; +import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization; + +/** + * ScatterPlotProjector is responsible for producing a set of scatterplot + * visualizations. + * + * @author Erich Schubert + * + * @apiviz.uses ScalesResult + * @apiviz.uses Projection1D + * + * @param <V> Vector type + */ +public class HistogramProjector<V extends NumberVector> implements Projector { + /** + * Relation we project. + */ + Relation<V> rel; + + /** + * Database dimensionality. + */ + int dmax; + + /** + * Constructor. + * + * @param rel Relation + * @param maxdim Maximum dimension to use + */ + public HistogramProjector(Relation<V> rel, int maxdim) { + super(); + this.rel = rel; + this.dmax = maxdim; + assert(maxdim <= RelationUtil.dimensionality(rel)) : "Requested dimensionality larger than data dimensionality?!?"; + } + + @Override + public Collection<PlotItem> arrange(VisualizerContext context) { + List<PlotItem> layout = new ArrayList<>(1 + dmax); + List<VisualizationTask> tasks = context.getVisTasks(this); + if(tasks.size() > 0) { + final double xoff = (dmax > 1) ? .1 : 0.; + final double hheight = .5; + final double lheight = .1; + PlotItem master = new PlotItem(dmax + xoff, hheight + lheight, null); + ScalesResult scales = ResultUtil.getScalesResult(rel); + for(int d1 = 0; d1 < dmax; d1++) { + Projection1D proj = new Simple1D(this, scales.getScales(), d1); + final PlotItem it = new PlotItem(d1 + xoff, lheight, 1., hheight, proj); + it.tasks = tasks; + master.subitems.add(it); + } + layout.add(master); + // Add labels + for(int d1 = 0; d1 < dmax; d1++) { + PlotItem it = new PlotItem(d1 + xoff, 0, 1., lheight, null); + LabelVisualization lbl = new LabelVisualization(RelationUtil.getColumnLabel(rel, d1)); + final VisualizationTask task = new VisualizationTask("", context, null, null, lbl); + task.reqheight = lheight; + task.reqwidth = 1; + task.addFlags(VisualizationTask.FLAG_NO_DETAIL); + it.tasks.add(task); + master.subitems.add(it); + } + } + return layout; + } + + @Override + public String getMenuName() { + return "Axis plot"; + } + + /** + * Get the relation we project. + * + * @return Relation + */ + public Relation<V> getRelation() { + return rel; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjector.java new file mode 100644 index 00000000..2e615c89 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjector.java @@ -0,0 +1,101 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; +import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.OPTICSProjection; + +/** + * Projection for OPTICS plots. + * + * @author Erich Schubert + */ +public class OPTICSProjector implements Projector { + /** + * Cluster order result + */ + private ClusterOrder clusterOrder; + + /** + * OPTICS plot image + */ + private OPTICSPlot plot = null; + + /** + * Constructor. + * + * @param co Cluster order + */ + public OPTICSProjector(ClusterOrder co) { + super(); + this.clusterOrder = co; + } + + @Override + public String getMenuName() { + return "OPTICS Plot Projection"; + } + + @Override + public Collection<PlotItem> arrange(VisualizerContext context) { + List<PlotItem> col = new ArrayList<>(1); + List<VisualizationTask> tasks = context.getVisTasks(this); + if(tasks.size() > 0) { + final PlotItem it = new PlotItem(4., 1., new OPTICSProjection(this)); + it.tasks = tasks; + col.add(it); + } + return col; + } + + /** + * Get the cluster order + * + * @return the cluster order + */ + public ClusterOrder getResult() { + return clusterOrder; + } + + /** + * Get or produce the actual OPTICS plot. + * + * @param context Context to use + * @return Plot + */ + public OPTICSPlot getOPTICSPlot(VisualizerContext context) { + if(plot == null) { + plot = OPTICSPlot.plotForClusterOrder(clusterOrder, context); + } + return plot; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjectorFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjectorFactory.java new file mode 100644 index 00000000..32a0e540 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjectorFactory.java @@ -0,0 +1,54 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; + +/** + * Produce OPTICS plot projections + * + * @author Erich Schubert + * + * @apiviz.has OPTICSProjector + */ +public class OPTICSProjectorFactory implements ProjectorFactory { + /** + * Constructor. + */ + public OPTICSProjectorFactory() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ClusterOrder> it1 = VisualizationTree.filterResults(context, start, ClusterOrder.class); + for(; it1.valid(); it1.advance()) { + final ClusterOrder or = it1.get(); + context.addVis(or, new OPTICSProjector(or)); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotFactory.java new file mode 100644 index 00000000..1bf12d79 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotFactory.java @@ -0,0 +1,94 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; + +/** + * Produce parallel axes projections. + * + * @author Robert Rödler + * + * @apiviz.has ParallelPlotProjector + */ +public class ParallelPlotFactory implements ProjectorFactory { + /** + * Constructor. + */ + public ParallelPlotFactory() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<Relation<?>> it = VisualizationTree.filterResults(context, start, Relation.class); + candidate: for(; it.valid(); it.advance()) { + Relation<?> rel = it.get(); + // TODO: multi-relational parallel plots? + final int dim = dimensionality(rel); + if(dim <= 1) { + continue; + } + // Do not enable nested relations by default: + Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<>(context.getHierarchy().iterAncestors(rel), Relation.class); + for(; it2.valid(); it2.advance()) { + // Parent relation + final Relation<?> rel2 = (Relation<?>) it2.get(); + final int odim = dimensionality(rel2); + if(odim == dim) { + // TODO: add Actions instead? + continue candidate; + } + } + @SuppressWarnings("unchecked") + Relation<SpatialComparable> vrel = (Relation<SpatialComparable>) rel; + ParallelPlotProjector<SpatialComparable> proj = new ParallelPlotProjector<>(vrel); + context.addVis(vrel, proj); + } + } + + private int dimensionality(Relation<?> rel) { + if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + @SuppressWarnings("unchecked") + Relation<NumberVector> vrel = (Relation<NumberVector>) rel; + return RelationUtil.dimensionality(vrel); + } + if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + @SuppressWarnings("unchecked") + Relation<UncertainObject> vrel = (Relation<UncertainObject>) rel; + return RelationUtil.dimensionality(vrel); + } + // TODO: allow other spatial objects of fixed dimensionality! + return 0; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotProjector.java new file mode 100644 index 00000000..28069cc4 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotProjector.java @@ -0,0 +1,95 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.ScalesResult; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; +import de.lmu.ifi.dbs.elki.visualization.projections.ProjectionParallel; +import de.lmu.ifi.dbs.elki.visualization.projections.SimpleParallel; + +/** + * ParallelPlotProjector is responsible for producing a parallel axes + * visualization. + * + * @author Robert Rödler + * + * @param <V> Vector type + */ +// TODO: support categorical features, and multiple relations too +public class ParallelPlotProjector<V extends SpatialComparable> implements Projector { + /** + * Relation we project. + */ + Relation<V> rel; + + /** + * Constructor. + * + * @param rel Relation + */ + public ParallelPlotProjector(Relation<V> rel) { + super(); + this.rel = rel; + } + + @Override + public Collection<PlotItem> arrange(VisualizerContext context) { + List<PlotItem> col = new ArrayList<>(1); + List<VisualizationTask> tasks = context.getVisTasks(this); + if(tasks.size() > 0) { + ScalesResult scales = ResultUtil.getScalesResult(rel); + ProjectionParallel proj = new SimpleParallel(this, scales.getScales()); + + final double width = Math.max(.5, Math.ceil(MathUtil.log2(scales.getScales().length - 1))); + final PlotItem it = new PlotItem(width, 1., proj); + it.tasks = tasks; + col.add(it); + } + return col; + } + + @Override + public String getMenuName() { + return "Parallelplot"; + } + + /** + * The relation we project. + * + * @return Relation + */ + public Relation<V> getRelation() { + return rel; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/Projector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/Projector.java new file mode 100644 index 00000000..b726dfef --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/Projector.java @@ -0,0 +1,45 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Collection; + +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; + +/** + * A projector is responsible for adding projections to the visualization. + * + * @author Erich Schubert + */ +public interface Projector extends VisualizationItem { + /** + * Produce an arrangement of projections. + * + * @param context Visualization context + * @return Arrangement. + */ + public Collection<PlotItem> arrange(VisualizerContext context); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ProjectorFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ProjectorFactory.java new file mode 100644 index 00000000..10ad8bd9 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ProjectorFactory.java @@ -0,0 +1,44 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; + +/** + * A projector is responsible for adding projections to the visualization by + * detecting appropriate relations in the database. + * + * @author Erich Schubert + * + * @apiviz.has Projector + */ +public interface ProjectorFactory extends VisualizationProcessor { + /** + * Add projections for the given result (tree) to the result tree. + * @param context Visualization context + * @param start Result to process + */ + @Override + public void processNewResult(VisualizerContext context, Object start); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotFactory.java new file mode 100644 index 00000000..785ea672 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotFactory.java @@ -0,0 +1,151 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +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.constraints.CommonConstraints; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; + +/** + * Produce scatterplot projections. + * + * @author Erich Schubert + * + * @apiviz.has ScatterPlotProjector + */ +public class ScatterPlotFactory implements ProjectorFactory { + /** + * Maximum number of dimensions to visualize. + * + * FIXME: add scrolling function for higher dimensionality! + */ + public static final int MAX_DIMENSIONS_DEFAULT = 10; + + /** + * Stores the maximum number of dimensions to show. + */ + private int maxdim = MAX_DIMENSIONS_DEFAULT; + + /** + * Constructor. + * + * @param maxdim Maximum number of dimensions to show. + */ + public ScatterPlotFactory(int maxdim) { + super(); + this.maxdim = maxdim; + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<Relation<?>> it = VisualizationTree.filterResults(context, start, Relation.class); + candidate: for(; it.valid(); it.advance()) { + Relation<?> rel = it.get(); + final int dim = dimensionality(rel); + if(dim < 1) { + continue; + } + // Do not enable nested relations by default: + Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<>(context.getHierarchy().iterAncestors(rel), Relation.class); + for(; it2.valid(); it2.advance()) { + // Parent relation + final Relation<?> rel2 = (Relation<?>) it2.get(); + final int odim = dimensionality(rel2); + if(odim == dim) { + // TODO: add Actions instead? + continue candidate; + } + } + @SuppressWarnings("unchecked") + Relation<SpatialComparable> vrel = (Relation<SpatialComparable>) rel; + ScatterPlotProjector<SpatialComparable> proj = new ScatterPlotProjector<>(vrel, Math.min(maxdim, dim)); + context.addVis(vrel, proj); + } + } + + private int dimensionality(Relation<?> rel) { + if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + @SuppressWarnings("unchecked") + Relation<NumberVector> vrel = (Relation<NumberVector>) rel; + return RelationUtil.dimensionality(vrel); + } + if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + @SuppressWarnings("unchecked") + Relation<UncertainObject> vrel = (Relation<UncertainObject>) rel; + return RelationUtil.dimensionality(vrel); + } + // TODO: allow other spatial objects of fixed dimensionality! + return 0; + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Parameter for the maximum number of dimensions. + * + * <p> + * Code: -vis.maxdim + * </p> + */ + public static final OptionID MAXDIM_ID = new OptionID("vis.maxdim", "Maximum number of dimensions to display."); + + /** + * Stores the maximum number of dimensions to show. + */ + private int maxdim = MAX_DIMENSIONS_DEFAULT; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + IntParameter maxdimP = new IntParameter(MAXDIM_ID, MAX_DIMENSIONS_DEFAULT); + maxdimP.addConstraint(CommonConstraints.GREATER_EQUAL_ZERO_INT); + if(config.grab(maxdimP)) { + maxdim = maxdimP.intValue(); + } + } + + @Override + protected ScatterPlotFactory makeInstance() { + return new ScatterPlotFactory(maxdim); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotProjector.java new file mode 100644 index 00000000..b8705a39 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotProjector.java @@ -0,0 +1,180 @@ +package de.lmu.ifi.dbs.elki.visualization.projector; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.math.linearalgebra.AffineTransformation; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.ScalesResult; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem; +import de.lmu.ifi.dbs.elki.visualization.projections.AffineProjection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; +import de.lmu.ifi.dbs.elki.visualization.projections.Simple2D; +import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization; + +/** + * ScatterPlotProjector is responsible for producing a set of scatterplot + * visualizations. + * + * @author Erich Schubert + * + * @apiviz.uses ScalesResult + * @apiviz.uses Projection2D + * + * @param <V> Vector type + */ +public class ScatterPlotProjector<V extends SpatialComparable> implements Projector { + /** + * Relation we project. + */ + Relation<V> rel; + + /** + * Database dimensionality. + */ + int dmax; + + /** + * Constructor. + * + * @param rel Relation + * @param maxdim Maximum dimension to use + */ + public ScatterPlotProjector(Relation<V> rel, int maxdim) { + super(); + this.rel = rel; + this.dmax = maxdim; + assert(maxdim <= RelationUtil.dimensionality(rel)) : "Requested dimensionality larger than data dimensionality?!?"; + } + + @Override + public Collection<PlotItem> arrange(VisualizerContext context) { + List<PlotItem> layout = new ArrayList<>(1); + List<VisualizationTask> tasks = context.getVisTasks(this); + if(tasks.size() > 0) { + ScalesResult scales = ResultUtil.getScalesResult(rel); + final PlotItem master; + if(dmax == 2) { + // In 2d, make the plot twice as big. + master = new PlotItem(2 + .1, 2 + .1, null); + { + Projection2D proj = new Simple2D(this, scales.getScales(), 0, 1); + PlotItem it = new PlotItem(.1, 0, 2., 2., proj); + it.tasks = tasks; + master.subitems.add(it); + } + // Label at bottom + { + PlotItem it = new PlotItem(.1, 2., 2., .1, null); + final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, 0))); + task.reqheight = .1; + task.reqwidth = 2.; + task.addFlags(VisualizationTask.FLAG_NO_DETAIL); + it.tasks.add(task); + master.subitems.add(it); + } + // Label on left + { + PlotItem it = new PlotItem(0, 0, .1, 2, null); + final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, 1), true)); + task.reqheight = 2.; + task.reqwidth = .1; + task.addFlags(VisualizationTask.FLAG_NO_DETAIL); + it.tasks.add(task); + master.subitems.add(it); + } + } + else { + final double sizeh = Math.ceil((dmax - 1) / 2.0); + master = new PlotItem(sizeh * 2. + .1, dmax - 1 + .1, null); + + for(int d1 = 0; d1 < dmax - 1; d1++) { + for(int d2 = d1 + 1; d2 < dmax; d2++) { + Projection2D proj = new Simple2D(this, scales.getScales(), d1, d2); + PlotItem it = new PlotItem(d1 + .1, d2 - 1, 1., 1., proj); + it.tasks = tasks; + master.subitems.add(it); + } + } + if(dmax >= 3) { + AffineTransformation p = AffineProjection.axisProjection(RelationUtil.dimensionality(rel), 1, 2); + p.addRotation(0, 2, Math.PI / 180 * -10.); + p.addRotation(1, 2, Math.PI / 180 * 15.); + // Wanna try 4d? go ahead: + // p.addRotation(0, 3, Math.PI / 180 * -20.); + // p.addRotation(1, 3, Math.PI / 180 * 30.); + Projection2D proj = new AffineProjection(this, scales.getScales(), p); + PlotItem it = new PlotItem(sizeh + .1, 0, sizeh, sizeh, proj); + it.tasks = tasks; + master.subitems.add(it); + } + // Labels at bottom + for(int d1 = 0; d1 < dmax - 1; d1++) { + PlotItem it = new PlotItem(d1 + .1, dmax - 1, 1., .1, null); + final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, d1))); + task.reqheight = .1; + task.reqwidth = 1; + task.addFlags(VisualizationTask.FLAG_NO_DETAIL); + it.tasks.add(task); + master.subitems.add(it); + } + // Labels on left + for(int d2 = 1; d2 < dmax; d2++) { + PlotItem it = new PlotItem(0, d2 - 1, .1, 1, null); + final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, d2), true)); + task.reqheight = 1; + task.reqwidth = .1; + task.addFlags(VisualizationTask.FLAG_NO_DETAIL); + it.tasks.add(task); + master.subitems.add(it); + } + } + + layout.add(master); + } + return layout; + } + + @Override + public String getMenuName() { + return "Scatterplot"; + } + + /** + * The relation we project. + * + * @return Relation + */ + public Relation<V> getRelation() { + return rel; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/package-info.java new file mode 100755 index 00000000..35a0e9a2 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Projectors are responsible for finding appropriate projections for data relations.</p> + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.projector;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SVGSaveDialog.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SVGSaveDialog.java new file mode 100644 index 00000000..345e1b68 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SVGSaveDialog.java @@ -0,0 +1,198 @@ +package de.lmu.ifi.dbs.elki.visualization.savedialog; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.Component; +import java.io.File; +import java.io.IOException; + +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactoryConfigurationError; + +import org.apache.batik.transcoder.TranscoderException; + +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.utilities.FileUtil; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * A save dialog to save/export a SVG image to a file. + * + * Supported formats: + * <ul> + * <li>JPEG (broken) (with width and height options)</li> + * <li>PNG (with width and height options)</li> + * <li>PDF</li> + * <li>PS</li> + * <li>EPS</li> + * <li>SVG</li> + * </ul> + * + * @author Simon Mittermüller + * + * @apiviz.composedOf SaveOptionsPanel + */ +public class SVGSaveDialog { + /** The default title. "Save as ...". */ + public static final String DEFAULT_TITLE = "Save as ..."; + + /** Static logger reference */ + private static final Logging LOG = Logging.getLogger(SVGSaveDialog.class); + + /** Automagic file format */ + final static String automagic_format = "automatic"; + + /** Supported file format (extensions) */ + final static String[] formats; + + /** Visible file formats */ + final static String[] visibleformats; + + static { + // FOP installed? + if (SVGPlot.hasFOPInstalled()) { + formats = new String[] { "svg", "png", "jpeg", "jpg", "pdf", "ps", "eps" }; + visibleformats = new String[] { automagic_format, "svg", "png", "jpeg", "pdf", "ps", "eps" }; + } else { + formats = new String[] { "svg", "png", "jpeg", "jpg" }; + visibleformats = new String[] { automagic_format, "svg", "png", "jpeg" }; + } + } + + /** + * Show a "Save as" dialog. + * + * @param plot The plot to be exported. + * @param width The width of the exported image (when export to JPEG/PNG). + * @param height The height of the exported image (when export to JPEG/PNG). + * @return Result from {@link JFileChooser#showSaveDialog} + */ + public static int showSaveDialog(SVGPlot plot, int width, int height) { + double quality = 1.0; + int ret = -1; + + JFileChooser fc = new JFileChooser(new File(".")); + fc.setDialogTitle(DEFAULT_TITLE); + // fc.setFileFilter(new ImageFilter()); + SaveOptionsPanel optionsPanel = new SaveOptionsPanel(fc, width, height); + fc.setAccessory(optionsPanel); + + ret = fc.showSaveDialog(null); + fc.setDialogTitle("Saving... Please wait."); + if (ret == JFileChooser.APPROVE_OPTION) { + File file = fc.getSelectedFile(); + String format = optionsPanel.getSelectedFormat(); + if (format == null || automagic_format.equals(format)) { + format = guessFormat(file.getName()); + } + try { + if (format == null) { + showError(fc, "Error saving image.", "File format not recognized."); + } else if ("jpeg".equals(format) || "jpg".equals(format)) { + quality = optionsPanel.getJPEGQuality(); + plot.saveAsJPEG(file, width, height, quality); + } else if ("png".equals(format)) { + plot.saveAsPNG(file, width, height); + } else if ("ps".equals(format)) { + plot.saveAsPS(file); + } else if ("eps".equals(format)) { + plot.saveAsEPS(file); + } else if ("pdf".equals(format)) { + plot.saveAsPDF(file); + } else if ("svg".equals(format)) { + plot.saveAsSVG(file); + } else { + showError(fc, "Error saving image.", "Unsupported format: " + format); + } + } catch (java.lang.IncompatibleClassChangeError e) { + showError(fc, "Error saving image.", "It seems that your Java version is incompatible with this version of Batik and Jpeg writing. Sorry."); + } catch (ClassNotFoundException e) { + showError(fc, "Error saving image.", "A class was not found when saving this image. Maybe installing Apache FOP will help (for PDF, PS and EPS output).\n" + e.toString()); + } catch (IOException e) { + LOG.exception(e); + showError(fc, "Error saving image.", e.toString()); + } catch (TranscoderException e) { + LOG.exception(e); + showError(fc, "Error saving image.", e.toString()); + } catch (TransformerFactoryConfigurationError e) { + LOG.exception(e); + showError(fc, "Error saving image.", e.toString()); + } catch (TransformerException e) { + LOG.exception(e); + showError(fc, "Error saving image.", e.toString()); + } catch (Exception e) { + LOG.exception(e); + showError(fc, "Error saving image.", e.toString()); + } + } else if (ret == JFileChooser.ERROR_OPTION) { + showError(fc, "Error in file dialog.", "Unknown Error."); + } else if (ret == JFileChooser.CANCEL_OPTION) { + // do nothing - except return result + } + return ret; + } + + /** + * Guess a supported format from the file name. For "auto" format handling. + * + * @param name File name + * @return format or "null" + */ + public static String guessFormat(String name) { + String ext = FileUtil.getFilenameExtension(name); + for (String format : formats) { + if (format.equals(ext)) { + return ext; + } + } + return null; + } + + /** + * @return the formats + */ + public static String[] getFormats() { + return formats; + } + + /** + * @return the visibleformats + */ + public static String[] getVisibleFormats() { + return visibleformats; + } + + /** + * Helper method to show a error message as "popup". Calls + * {@link JOptionPane#showMessageDialog(java.awt.Component, Object)}. + * + * @param parent The parent component for the popup. + * @param msg The message to be displayed. + * */ + private static void showError(Component parent, String title, String msg) { + JOptionPane.showMessageDialog(parent, msg, title, JOptionPane.ERROR_MESSAGE); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SaveOptionsPanel.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SaveOptionsPanel.java new file mode 100644 index 00000000..6e45ac99 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SaveOptionsPanel.java @@ -0,0 +1,324 @@ +package de.lmu.ifi.dbs.elki.visualization.savedialog; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * A component (JPanel) which can be displayed in the save dialog to show + * additional options when saving as JPEG or PNG. + * + * @author Simon Mittermüller + */ +public class SaveOptionsPanel extends JPanel { + // TODO: externalize strings + private static final String STR_IMAGE_SIZE = "Size options:"; + + private static final String STR_JPEG_QUALITY = "Quality:"; + + private static final String STR_IMAGE_HEIGHT = "Height:"; + + private static final String STR_IMAGE_WIDTH = "Width:"; + + private static final String STR_CHOOSE_FORMAT = "Choose format:"; + + private static final String STR_RESET_IMAGE_SIZE = "Reset image size"; + + private static final String STR_LOCK_ASPECT_RATIO = "Lock aspect ratio"; + + /** + * Serial version. + */ + private static final long serialVersionUID = 1L; + + /** The fileChooser on which this panel is installed. */ + private JFileChooser fc; + + /** + * The width of the exported image (if exported to JPEG or PNG). Default is + * 1024. + */ + protected int width = 1024; + + /** + * The height of the exported image (if exported to JPEG or PNG). Default is + * 768. + */ + protected int height = 768; + + /** Ratio for easier size adjustment */ + double ratio = 16.0 / 10.0; + + /** Main panel */ + private JPanel mainPanel; + + /** Shows quality info when saving as JPEG. */ + private JPanel qualPanel; + + /** If saving as JPEG/PNG show width/height infos here. */ + private JPanel sizePanel; + + protected JSpinner spinnerWidth; + + protected JSpinner spinnerHeight; + + protected JSpinner spinnerQual; + + protected SpinnerNumberModel modelWidth; + + protected SpinnerNumberModel modelHeight; + + protected SpinnerNumberModel modelQuality; + + protected JCheckBox aspectRatioLock; + + protected JButton resetSizeButton; + + protected JComboBox<String> formatSelector; + + // Not particularly useful for most - hide it for now. + private final boolean hasResetButton = false; + + /** + * Construct a new Save Options Panel. + * + * @param fc File chooser to display in + * @param w Default image width + * @param h Default image height + */ + public SaveOptionsPanel(JFileChooser fc, int w, int h) { + this.width = w; + this.height = h; + this.ratio = (double) w / (double) h; + this.fc = fc; + + mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS)); + + sizePanel = new JPanel(); + sizePanel.setLayout(new BoxLayout(sizePanel, BoxLayout.Y_AXIS)); + sizePanel.setBorder(BorderFactory.createTitledBorder(STR_IMAGE_SIZE)); + + // *** Format panel + mainPanel.add(new JLabel(STR_CHOOSE_FORMAT)); + + formatSelector = new JComboBox<>(SVGSaveDialog.getVisibleFormats()); + formatSelector.setSelectedIndex(0); + formatSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if(e.getItem() != null) { + String format = (String) formatSelector.getSelectedItem(); + setFormat(format); + } + } + }); + mainPanel.add(formatSelector); + + // *** Size panel + JPanel widthPanel = new JPanel(); + JPanel heightPanel = new JPanel(); + widthPanel.add(new JLabel(STR_IMAGE_WIDTH)); + heightPanel.add(new JLabel(STR_IMAGE_HEIGHT)); + + // size models + modelWidth = new SpinnerNumberModel(width, 0, 100000, 1); + modelHeight = new SpinnerNumberModel(height, 0, 100000, 1); + + // size spinners + spinnerWidth = new JSpinner(modelWidth); + spinnerWidth.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if(aspectRatioLock.isSelected()) { + int val = modelWidth.getNumber().intValue(); + spinnerHeight.setValue(new Integer((int) Math.round(val / ratio))); + } + } + }); + widthPanel.add(spinnerWidth); + + spinnerHeight = new JSpinner(modelHeight); + spinnerHeight.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if(aspectRatioLock.isSelected()) { + int val = modelHeight.getNumber().intValue(); + spinnerWidth.setValue(new Integer((int) Math.round(val * ratio))); + } + } + }); + heightPanel.add(spinnerHeight); + + // add subpanels + sizePanel.add(widthPanel); + sizePanel.add(heightPanel); + + // aspect lock + aspectRatioLock = new JCheckBox(STR_LOCK_ASPECT_RATIO); + aspectRatioLock.setSelected(true); + // aspectRatioLock.addActionListener(x); + sizePanel.add(aspectRatioLock); + + // reset size button + if(hasResetButton) { + resetSizeButton = new JButton(STR_RESET_IMAGE_SIZE); + resetSizeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + modelWidth.setValue(Integer.valueOf(width)); + modelHeight.setValue(Integer.valueOf(height)); + aspectRatioLock.setSelected(true); + } + }); + sizePanel.add(resetSizeButton); + } + + mainPanel.add(sizePanel); + + // Quality settings panel + qualPanel = new JPanel(); + // quality settings will not be visible by default (JPEG only) + qualPanel.setVisible(false); + qualPanel.setLayout(new BoxLayout(qualPanel, BoxLayout.Y_AXIS)); + qualPanel.setBorder(BorderFactory.createTitledBorder(STR_JPEG_QUALITY)); + modelQuality = new SpinnerNumberModel(0.7, 0.1, 1.0, 0.1); + spinnerQual = new JSpinner(modelQuality); + // spinnerQual.addChangeListener(x); + qualPanel.add(spinnerQual); + + mainPanel.add(qualPanel); + + add(mainPanel); + + // setup a listener to react to file name changes + this.fc.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent e) { + if(e.getPropertyName().equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) { + File file = (File) e.getNewValue(); + if(file != null && file.getName() != null) { + String format = SVGSaveDialog.guessFormat(file.getName()); + if(format != null) { + setFormat(format); + } + } + } + } + }); + } + + protected void setFormat(String format) { + String[] formats = SVGSaveDialog.getVisibleFormats(); + int index = -1; + for(int i = 0; i < formats.length; i++) { + if(formats[i].equals(format)) { + index = i; + break; + } + } + if(index != formatSelector.getSelectedIndex() && index >= 0) { + formatSelector.setSelectedIndex(index); + } + if(format.equals("jpeg") || format.equals("jpg")) { + sizePanel.setVisible(true); + qualPanel.setVisible(true); + } + else if(format.equals("png")) { + sizePanel.setVisible(true); + qualPanel.setVisible(false); + } + else if(format.equals("pdf")) { + sizePanel.setVisible(false); + qualPanel.setVisible(false); + mainPanel.validate(); + } + else if(format.equals("ps")) { + sizePanel.setVisible(false); + qualPanel.setVisible(false); + mainPanel.validate(); + } + else if(format.equals("eps")) { + sizePanel.setVisible(false); + qualPanel.setVisible(false); + mainPanel.validate(); + } + else if(format.equals("svg")) { + sizePanel.setVisible(false); + qualPanel.setVisible(false); + mainPanel.validate(); + } + else { + // TODO: what to do on unknown formats? + // LoggingUtil.warning("Unrecognized file extension seen: " + format); + } + } + + /** + * Return the selected file format. + * + * @return file format identification + */ + public String getSelectedFormat() { + String format = (String) formatSelector.getSelectedItem(); + return format; + } + + /** + * Returns the quality value in the quality field. + * + * It is ensured that return value is in the range of [0:1] + * + * @return Quality value for JPEG. + */ + public double getJPEGQuality() { + double qual =modelQuality.getNumber().doubleValue(); + if(qual > 1.0) { + qual = 1.0; + } + if(qual < 0) { + qual = 0.0; + } + return qual; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/package-info.java new file mode 100644 index 00000000..5b56c4ff --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Save dialog for SVG plots.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.savedialog;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClassStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClassStylingPolicy.java new file mode 100644 index 00000000..7bc50182 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClassStylingPolicy.java @@ -0,0 +1,74 @@ +package de.lmu.ifi.dbs.elki.visualization.style; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; + +/** + * Styling policy that is based on <em>classes</em>, for example clusters or + * labels. This allows for certain optimizations such as marker reuse, and thus + * is preferred when possible. + * + * @author Erich Schubert + */ +public interface ClassStylingPolicy extends StylingPolicy { + /** + * Get the style number for a particular object + * + * @param id Object ID + * @return Style number + */ + int getStyleForDBID(DBIDRef id); + + /** + * Get the minimum style in use. + * + * @return Style number + */ + int getMinStyle(); + + /** + * Get the maximum style in use. + * + * @return Style number + */ + int getMaxStyle(); + + /** + * Iterate over all objects from a given class. + * + * @param cnum Class number + * @return Iterator over object IDs + */ + DBIDIter iterateClass(int cnum); + + /** + * Get the number of elements in the styling class. + * + * @param cnum Class number + * @return Size of class. + */ + int classSize(int cnum); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClusterStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClusterStylingPolicy.java new file mode 100644 index 00000000..643f4328 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClusterStylingPolicy.java @@ -0,0 +1,167 @@ +package de.lmu.ifi.dbs.elki.visualization.style; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import gnu.trove.list.array.TIntArrayList; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Styling policy based on cluster membership. + * + * @author Erich Schubert + * + */ +// TODO: fast enough? Some other kind of mapping we can use? +public class ClusterStylingPolicy implements ClassStylingPolicy { + /** + * Object IDs + */ + ArrayList<DBIDs> ids; + + /** + * Map from cluster objects to color offsets. + */ + TObjectIntMap<Cluster<?>> cmap; + + /** + * Colors + */ + TIntArrayList colors; + + /** + * Clustering in use. + */ + Clustering<?> clustering; + + /** + * Constructor. + * + * @param clustering Clustering to use. + */ + public ClusterStylingPolicy(Clustering<?> clustering, StyleLibrary style) { + super(); + this.clustering = clustering; + ColorLibrary colorset = style.getColorSet(StyleLibrary.PLOT); + List<? extends Cluster<?>> clusters = clustering.getAllClusters(); + ids = new ArrayList<>(clusters.size()); + colors = new TIntArrayList(clusters.size()); + cmap = new TObjectIntHashMap<>(clusters.size(), .5f, -1); + + Iterator<? extends Cluster<?>> ci = clusters.iterator(); + for (int i = 0; ci.hasNext(); i++) { + Cluster<?> c = ci.next(); + ids.add(DBIDUtil.ensureSet(c.getIDs())); + cmap.put(c, i); + Color col = SVGUtil.stringToColor(colorset.getColor(i)); + if (col != null) { + colors.add(col.getRGB()); + } else { + LoggingUtil.warning("Unrecognized color name: " + colorset.getColor(i)); + } + if (!ci.hasNext()) { + break; + } + } + } + + @Override + public int getStyleForDBID(DBIDRef id) { + for (int i = 0; i < ids.size(); i++) { + if (ids.get(i).contains(id)) { + return i; + } + } + return -1; + } + + @Override + public int getColorForDBID(DBIDRef id) { + for (int i = 0; i < ids.size(); i++) { + if (ids.get(i).contains(id)) { + return colors.get(i); + } + } + return 0; + } + + @Override + public int getMinStyle() { + return 0; + } + + @Override + public int getMaxStyle() { + return ids.size(); + } + + @Override + public DBIDIter iterateClass(int cnum) { + return ids.get(cnum).iter(); + } + + @Override + public int classSize(int cnum) { + return ids.get(cnum).size(); + } + + /** + * Get the clustering used by this styling policy + * + * @return Clustering in use + */ + public Clustering<?> getClustering() { + return clustering; + } + + /** + * Get the style number for a cluster. + * + * @param c Cluster + * @return Style number + */ + public int getStyleForCluster(Cluster<?> c) { + return cmap.get(c); + } + + @Override + public String getMenuName() { + return clustering.getLongName(); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/PropertiesBasedStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/PropertiesBasedStyleLibrary.java new file mode 100644 index 00000000..c8440067 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/PropertiesBasedStyleLibrary.java @@ -0,0 +1,344 @@ +package de.lmu.ifi.dbs.elki.visualization.style; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.utilities.FileUtil; +import de.lmu.ifi.dbs.elki.utilities.FormatUtil; +import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.colors.ListBasedColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.lines.SolidLineStyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.marker.PrettyMarkers; + +/** + * Style library loading the parameters from a properties file. + * + * @author Erich Schubert + */ +// TODO: also use Caching for String values? +public class PropertiesBasedStyleLibrary implements StyleLibrary { + /** + * Logger + */ + private static final Logging LOG = Logging.getLogger(PropertiesBasedStyleLibrary.class); + + /** + * Name of the default color scheme. + */ + public static final String DEFAULT_SCHEME_NAME = "Default"; + + /** + * File name of the default color scheme. + */ + public static final String DEFAULT_SCHEME_FILENAME = "default"; + + /** + * File extension + */ + public static final String DEFAULT_PROPERTIES_EXTENSION = ".properties"; + + /** + * Default properties path + */ + private static final String DEFAULT_PROPERTIES_PATH = PropertiesBasedStyleLibrary.class.getPackage().getName().replace('.', File.separatorChar) + File.separatorChar; + + /** + * Separator for lists. + */ + public static final String LIST_SEPARATOR = ","; + + /** + * Property string for the line style library + */ + public static final String PROP_LINES_LIBRARY = "lines-library"; + + /** + * Property string for the marker style library + */ + public static final String PROP_MARKER_LIBRARY = "marker-library"; + + /** + * Properties file to use. + */ + private Properties properties; + + /** + * Style scheme name + */ + private String name; + + /** + * Cache + */ + private Map<String, Object> cache = new HashMap<>(); + + /** + * Line style library to use + */ + private LineStyleLibrary linelib = null; + + /** + * Marker library to use + */ + private MarkerLibrary markerlib = null; + + /** + * Constructor without a properties file name. + */ + public PropertiesBasedStyleLibrary() { + this(DEFAULT_SCHEME_FILENAME, DEFAULT_SCHEME_NAME); + } + + /** + * Constructor with a given file name. + * + * @param filename Name of properties file. + * @param name NAme for this style + */ + public PropertiesBasedStyleLibrary(String filename, String name) { + this.properties = new Properties(); + this.name = name; + InputStream stream = null; + try { + stream = FileUtil.openSystemFile(filename); + } catch (FileNotFoundException e) { + try { + stream = FileUtil.openSystemFile(filename + DEFAULT_PROPERTIES_EXTENSION); + } catch (FileNotFoundException e2) { + try { + stream = FileUtil.openSystemFile(DEFAULT_PROPERTIES_PATH + filename + DEFAULT_PROPERTIES_EXTENSION); + } catch (FileNotFoundException e3) { + throw new AbortException("Could not find style scheme file '" + filename + "' for scheme '" + name + "'!"); + } + } + } + try { + properties.load(stream); + } catch (Exception e) { + throw new AbortException("Error loading properties file " + filename + ".\n", e); + } + } + + /** + * Get the style scheme name. + * + * @return the name + */ + protected String getName() { + return name; + } + + /** + * Get a value from the cache (to avoid repeated parsing) + * + * @param <T> Type + * @param prefix Tree name + * @param postfix Property name + * @param cls Class restriction + * @return Resulting value + */ + private <T> T getCached(String prefix, String postfix, Class<T> cls) { + return cls.cast(cache.get(prefix + '.' + postfix)); + } + + /** + * Set a cache value + * + * @param <T> Type + * @param prefix Tree name + * @param postfix Property name + * @param data Data + */ + private <T> void setCached(String prefix, String postfix, T data) { + cache.put(prefix + '.' + postfix, data); + } + + /** + * Retrieve the property value for a particular path + type pair. + * + * @param prefix Path + * @param postfix Type + * @return Value + */ + protected String getPropertyValue(String prefix, String postfix) { + String ret = properties.getProperty(prefix + "." + postfix); + if (ret != null) { + // logger.debugFine("Found property: "+prefix + "." + + // postfix+" for "+prefix); + return ret; + } + int pos = prefix.length(); + while (pos > 0) { + pos = prefix.lastIndexOf('.', pos - 1); + if (pos <= 0) { + break; + } + ret = properties.getProperty(prefix.substring(0, pos) + '.' + postfix); + if (ret != null) { + // logger.debugFine("Found property: "+prefix.substring(0, pos) + "." + + // postfix+" for "+prefix); + return ret; + } + } + ret = properties.getProperty(postfix); + if (ret != null) { + // logger.debugFine("Found property: "+postfix+" for "+prefix); + return ret; + } + return null; + } + + @Override + public String getColor(String key) { + return getPropertyValue(key, COLOR); + } + + @Override + public String getBackgroundColor(String key) { + return getPropertyValue(key, BACKGROUND_COLOR); + } + + @Override + public String getTextColor(String key) { + return getPropertyValue(key, TEXT_COLOR); + } + + @Override + public ColorLibrary getColorSet(String key) { + ColorLibrary cl = getCached(key, COLORSET, ColorLibrary.class); + if (cl == null) { + String[] colors = getPropertyValue(key, COLORSET).split(LIST_SEPARATOR); + cl = new ListBasedColorLibrary(colors, "Default"); + setCached(key, COLORSET, cl); + } + return cl; + } + + @Override + public double getLineWidth(String key) { + Double lw = getCached(key, LINE_WIDTH, Double.class); + if (lw == null) { + try { + lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, LINE_WIDTH)) * SCALE); + } catch (NullPointerException e) { + throw new AbortException("Missing/invalid value in style library: " + key + '.' + LINE_WIDTH); + } + } + return lw.doubleValue(); + } + + @Override + public double getTextSize(String key) { + Double lw = getCached(key, TEXT_SIZE, Double.class); + if (lw == null) { + try { + lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, TEXT_SIZE)) * SCALE); + } catch (NullPointerException e) { + throw new AbortException("Missing/invalid value in style library: " + key + '.' + TEXT_SIZE); + } + } + return lw.doubleValue(); + } + + @Override + public String getFontFamily(String key) { + return getPropertyValue(key, FONT_FAMILY); + } + + @Override + public double getSize(String key) { + Double lw = getCached(key, GENERIC_SIZE, Double.class); + if (lw == null) { + try { + lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, GENERIC_SIZE)) * SCALE); + } catch (NullPointerException e) { + throw new AbortException("Missing/invalid value in style library: " + key + '.' + GENERIC_SIZE); + } + } + return lw.doubleValue(); + } + + @Override + public double getOpacity(String key) { + Double lw = getCached(key, OPACITY, Double.class); + if (lw == null) { + try { + lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, OPACITY))); + } catch (NullPointerException e) { + throw new AbortException("Missing/invalid value in style library: " + key + '.' + OPACITY); + } + } + return lw.doubleValue(); + } + + @Override + public LineStyleLibrary lines() { + if (linelib == null) { + String libname = properties.getProperty(PROP_LINES_LIBRARY, SolidLineStyleLibrary.class.getName()); + try { + Class<?> cls; + try { + cls = Class.forName(libname); + } catch (ClassNotFoundException e) { + cls = Class.forName(LineStyleLibrary.class.getPackage().getName() + '.' + libname); + } + linelib = (LineStyleLibrary) cls.getConstructor(StyleLibrary.class).newInstance(this); + } catch (Exception e) { + LOG.exception(e); + linelib = new SolidLineStyleLibrary(this); + } + } + return linelib; + } + + @Override + public MarkerLibrary markers() { + if (markerlib == null) { + String libname = properties.getProperty(PROP_MARKER_LIBRARY, PrettyMarkers.class.getName()); + try { + Class<?> cls; + try { + cls = Class.forName(libname); + } catch (ClassNotFoundException e) { + cls = Class.forName(MarkerLibrary.class.getPackage().getName() + '.' + libname); + } + markerlib = (MarkerLibrary) cls.getConstructor(StyleLibrary.class).newInstance(this); + } catch (Exception e) { + LOG.exception(e); + markerlib = new PrettyMarkers(this); + } + } + return markerlib; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/SingleObjectsStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/SingleObjectsStylingPolicy.java new file mode 100644 index 00000000..0e26bf80 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/SingleObjectsStylingPolicy.java @@ -0,0 +1,34 @@ +package de.lmu.ifi.dbs.elki.visualization.style; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +/** + * Styling policy based on assigning objects individual colors. + * + * @author Erich Schubert + */ +public interface SingleObjectsStylingPolicy extends StylingPolicy { + // TODO: finish and use, e.g. for outliers and density visualization + // TODO: do we need anything here beyond "not a class styling policy"? +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StyleLibrary.java new file mode 100755 index 00000000..5ca93be6 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StyleLibrary.java @@ -0,0 +1,277 @@ +package de.lmu.ifi.dbs.elki.visualization.style; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary; + +/** + * Style library interface. A style library allows the user to customize the + * visual rendering, for example for print media or screen presentation without + * having to change program code. + * + * @author Erich Schubert + * + * @apiviz.composedOf ColorLibrary + * @apiviz.composedOf LineStyleLibrary + * @apiviz.composedOf MarkerLibrary + */ +public interface StyleLibrary { + /** + * Default + */ + final static String DEFAULT = ""; + + /** + * Page + */ + final static String PAGE = "page"; + + /** + * Plot + */ + final static String PLOT = "plot"; + + /** + * Axis + */ + final static String AXIS = "axis"; + + /** + * Axis tick + */ + final static String AXIS_TICK = "axis.tick"; + + /** + * Axis minor tick + */ + final static String AXIS_TICK_MINOR = "axis.tick.minor"; + + /** + * Axis label + */ + final static String AXIS_LABEL = "axis.label"; + + /** + * Key + */ + final static String KEY = "key"; + + /** + * Clusterorder + */ + final static String CLUSTERORDER = "plot.clusterorder"; + + /** + * Margin + */ + final static String MARGIN = "margin"; + + /** + * Bubble size + */ + final static String BUBBLEPLOT = "plot.bubble"; + + /** + * Marker size + */ + final static String MARKERPLOT = "plot.marker"; + + /** + * Dot size + */ + final static String DOTPLOT = "plot.dot"; + + /** + * Grayed out objects + */ + final static String PLOTGREY = "plot.grey"; + + /** + * Reference points color and size + */ + final static String REFERENCE_POINTS = "plot.referencepoints"; + + /** + * Polygons style + */ + final static String POLYGONS = "plot.polygons"; + + /** + * Selection color and opacity + */ + final static String SELECTION = "plot.selection"; + + /** + * Selection color and opacity during selecting process + */ + final static String SELECTION_ACTIVE = "plot.selection.active"; + + /** + * Scaling constant. Keep in sync with + * {@link de.lmu.ifi.dbs.elki.visualization.projections.Projection#SCALE} + */ + public static final double SCALE = 100.0; + + /* ** Property types ** */ + /** + * Color + */ + final static String COLOR = "color"; + + /** + * Background color + */ + final static String BACKGROUND_COLOR = "background-color"; + + /** + * Text color + */ + final static String TEXT_COLOR = "text-color"; + + /** + * Color set + */ + final static String COLORSET = "colorset"; + + /** + * Line width + */ + final static String LINE_WIDTH = "line-width"; + + /** + * Text size + */ + final static String TEXT_SIZE = "text-size"; + + /** + * Font family + */ + final static String FONT_FAMILY = "font-family"; + + /** + * Generic size + */ + final static String GENERIC_SIZE = "size"; + + /** + * Opacity (transparency) + */ + final static String OPACITY = "opacity"; + + /** + * XY curve styling. + */ + static final String XYCURVE = "xycurve"; + + /** + * Retrieve a color for an item + * + * @param name Reference name + * @return color in CSS/SVG valid format: hexadecimal (#aabbcc) or names such + * as "red" + */ + public String getColor(String name); + + /** + * Retrieve background color for an item + * + * @param name Reference name + * @return color in CSS/SVG valid format: hexadecimal (#aabbcc) or names such + * as "red" + */ + public String getBackgroundColor(String name); + + /** + * Retrieve text color for an item + * + * @param name Reference name + * @return color in CSS/SVG valid format: hexadecimal (#aabbcc) or names such + * as "red" + */ + public String getTextColor(String name); + + /** + * Retrieve colorset for an item + * + * @param name Reference name + * @return color library + */ + public ColorLibrary getColorSet(String name); + + /** + * Get line width + * + * @param key Key + * @return line width as double + */ + public double getLineWidth(String key); + + /** + * Get generic size + * + * @param key Key + * @return size as double + */ + public double getSize(String key); + + /** + * Get text size + * + * @param key Key + * @return line width as double + */ + public double getTextSize(String key); + + /** + * Get font family + * + * @param key Key + * @return font family CSS string + */ + public String getFontFamily(String key); + + /** + * Get opacity + * + * @param key Key + * @return size as double + */ + public double getOpacity(String key); + + /** + * Get the line style library to use. + * + * @return line style library + */ + public LineStyleLibrary lines(); + + /** + * Get the marker library to use. + * + * @return marker library + */ + public MarkerLibrary markers(); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StylingPolicy.java new file mode 100644 index 00000000..406b6aff --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StylingPolicy.java @@ -0,0 +1,48 @@ +package de.lmu.ifi.dbs.elki.visualization.style; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; + +/** + * Styling policy. + * + * Implementations <em>must</em> implement either {@link ClassStylingPolicy} or + * {@link SingleObjectsStylingPolicy} interfaces, as most visualizers will only + * support these known interfaces. + * + * @author Erich Schubert + */ +public interface StylingPolicy extends VisualizationItem { + /** + * Get the color for an individual object. + * + * Note: if possible, use a class styling policy which can optimize better. + * + * @param id Object ID + * @return Color value + */ + public int getColorForDBID(DBIDRef id); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/DashedLineStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/DashedLineStyleLibrary.java new file mode 100644 index 00000000..49b8cc91 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/DashedLineStyleLibrary.java @@ -0,0 +1,159 @@ +package de.lmu.ifi.dbs.elki.visualization.style.lines; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.CSSConstants; + +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Line library using various dashed and dotted line styles. + * + * This library is particularly useful for black and white output. + * + * {@link LineStyleLibrary#FLAG_STRONG} will result in thicker lines. + * + * {@link LineStyleLibrary#FLAG_WEAK} will result in thinner and + * semi-transparent lines. + * + * {@link LineStyleLibrary#FLAG_INTERPOLATED} will result in shorter dashing + * patterns. + * + * @author Erich Schubert + * + * @apiviz.composedOf ColorLibrary + */ +public class DashedLineStyleLibrary implements LineStyleLibrary { + /** + * The style library we use for colors + */ + private ColorLibrary colors; + + /** Dash patterns to regularly use */ + private double[][] dashpatterns = { + // solid, no dashing + {}, + // half-half + { .5, .5 }, + // quarters + { .25, .25, .25, .25 }, + // alternating long-quart + { .75, .25 }, + // dash-dot + { .7, .1, .1, .1 }, }; + + /** Replacement for the solid pattern in 'interpolated' mode */ + private double[] solidreplacement = { .1, .1 }; + + private int dashnum = dashpatterns.length; + + /** + * Color of "uncolored" dots + */ + private String dotcolor; + + /** + * Color of "greyed out" dots + */ + private String greycolor; + + /** + * Constructor + * + * @param style Style library + */ + public DashedLineStyleLibrary(StyleLibrary style) { + super(); + this.colors = style.getColorSet(StyleLibrary.PLOT); + this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT); + this.greycolor = style.getColor(StyleLibrary.PLOTGREY); + } + + @Override + public void formatCSSClass(CSSClass cls, int style, double width, Object... flags) { + if(style == -2) { + cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, greycolor); + } + else if(style == -1) { + cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, dotcolor); + } + else { + cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, colors.getColor(style)); + } + boolean interpolated = false; + // process flavoring flags + for(Object flag : flags) { + if(LineStyleLibrary.FLAG_STRONG.equals(flag)) { + width = width * 1.5; + } + else if(LineStyleLibrary.FLAG_WEAK.equals(flag)) { + cls.setStatement(CSSConstants.CSS_STROKE_OPACITY_PROPERTY, ".50"); + width = width * 0.75; + } + else if(LineStyleLibrary.FLAG_INTERPOLATED.equals(flag)) { + interpolated = true; + } + } + cls.setStatement(CSSConstants.CSS_STROKE_WIDTH_PROPERTY, SVGUtil.fmt(width)); + // handle dashing + int styleflav = (style > 0) ? (style % dashnum) : (-style % dashnum); + if(!interpolated) { + double[] pat = dashpatterns[styleflav]; + assert (pat.length % 2 == 0); + if(pat.length > 0) { + StringBuilder pattern = new StringBuilder(); + for(int i = 0; i < pat.length; i++) { + if(i > 0) { + pattern.append(','); + } + pattern.append(SVGUtil.fmt(pat[i] * width * 30)); + // pattern.append("%"); + } + cls.setStatement(CSSConstants.CSS_STROKE_DASHARRAY_PROPERTY, pattern.toString()); + } + } + else { + double[] pat = dashpatterns[styleflav]; + if(styleflav == 0) { + pat = solidreplacement; + } + assert (pat.length % 2 == 0); + // TODO: add dotting. + if(pat.length > 0) { + StringBuilder pattern = new StringBuilder(); + for(int i = 0; i < pat.length; i++) { + if(i > 0) { + pattern.append(','); + } + pattern.append(SVGUtil.fmt(pat[i] * width)); + // pattern.append("%"); + } + cls.setStatement(CSSConstants.CSS_STROKE_DASHARRAY_PROPERTY, pattern.toString()); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/LineStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/LineStyleLibrary.java new file mode 100644 index 00000000..48212f16 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/LineStyleLibrary.java @@ -0,0 +1,76 @@ +package de.lmu.ifi.dbs.elki.visualization.style.lines; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; + +/** + * Interface to obtain CSS classes for plot lines. + * + * {@code meta} is a set of Objects, usually constants that may or may not be + * used by the {@link LineStyleLibrary} to generate variants of the style. + * + * Predefined meta flags that are usually supported are: + * <dl> + * <dt>{@link #FLAG_STRONG}</dt> + * <dd>Request a "stronger" version of the same style</dd> + * <dt>{@link #FLAG_WEAK}</dt> + * <dd>Request a "weaker" version of the same style</dd> + * <dt>{@link #FLAG_INTERPOLATED}</dt> + * <dd>Request an "interpolated" version of the same style (e.g. lighter or + * dashed)</dd> + * </dl> + * + * @author Erich Schubert + * + * @apiviz.uses CSSClass oneway + */ +public interface LineStyleLibrary { + /** + * Meta flag to request a 'stronger' version of the style + */ + public static final String FLAG_STRONG = "strong"; + + /** + * Meta flag to request a 'weaker' version of the style + */ + public static final String FLAG_WEAK = "weak"; + + /** + * Meta flag to request an 'interpolated' version of the style + */ + public static final String FLAG_INTERPOLATED = "interpolated"; + + /** + * Add the formatting statements to the given CSS class. + * + * Note: this can overwrite some existing properties of the CSS class. + * + * @param cls CSS class to modify + * @param style style number + * @param width line width + * @param meta meta objects to request line variants + */ + public void formatCSSClass(CSSClass cls, int style, double width, Object... meta); +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/SolidLineStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/SolidLineStyleLibrary.java new file mode 100644 index 00000000..61765f1a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/SolidLineStyleLibrary.java @@ -0,0 +1,106 @@ +package de.lmu.ifi.dbs.elki.visualization.style.lines; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.CSSConstants; + +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Line style library featuring solid lines for default styles only (combine + * with a color library to obtain enough classes!) + * + * {@link LineStyleLibrary#FLAG_STRONG} will result in thicker lines. + * + * {@link LineStyleLibrary#FLAG_WEAK} will result in thinner and + * semi-transparent lines. + * + * {@link LineStyleLibrary#FLAG_INTERPOLATED} will result in dashed lines. + * + * @author Erich Schubert + * + * @apiviz.composedOf ColorLibrary + */ +public class SolidLineStyleLibrary implements LineStyleLibrary { + /** + * Reference to the color library. + */ + private ColorLibrary colors; + + /** + * Color of "uncolored" dots + */ + private String dotcolor; + + /** + * Color of "greyed out" dots + */ + private String greycolor; + + /** + * Constructor. + * + * @param style Style library to use. + */ + public SolidLineStyleLibrary(StyleLibrary style) { + super(); + this.colors = style.getColorSet(StyleLibrary.PLOT); + this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT); + this.greycolor = style.getColor(StyleLibrary.PLOTGREY); + } + + @Override + public void formatCSSClass(CSSClass cls, int style, double width, Object... flags) { + if(style == -2) { + cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, greycolor); + } + else if(style == -1) { + cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, dotcolor); + } + else { + cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, colors.getColor(style)); + } + boolean interpolated = false; + // process flavoring flags + for(Object flag : flags) { + if(LineStyleLibrary.FLAG_STRONG.equals(flag)) { + width = width * 1.5; + } + else if(LineStyleLibrary.FLAG_WEAK.equals(flag)) { + cls.setStatement(CSSConstants.CSS_STROKE_OPACITY_PROPERTY, ".50"); + width = width * 0.75; + } + else if(LineStyleLibrary.FLAG_INTERPOLATED.equals(flag)) { + interpolated = true; + } + } + cls.setStatement(CSSConstants.CSS_STROKE_WIDTH_PROPERTY, SVGUtil.fmt(width)); + if(interpolated) { + cls.setStatement(CSSConstants.CSS_STROKE_DASHARRAY_PROPERTY, "" + SVGUtil.fmt(width / StyleLibrary.SCALE * 2.) + "," + SVGUtil.fmt(width / StyleLibrary.SCALE * 2.)); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/package-info.java new file mode 100755 index 00000000..69790fdf --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Generate line styles for plotting in CSS</p> + * + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.style.lines;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/CircleMarkers.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/CircleMarkers.java new file mode 100644 index 00000000..c4078091 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/CircleMarkers.java @@ -0,0 +1,90 @@ +package de.lmu.ifi.dbs.elki.visualization.style.marker; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Simple marker library that just draws colored circles at the given + * coordinates. + * + * @author Erich Schubert + * + * @apiviz.composedOf ColorLibrary + */ +public class CircleMarkers implements MarkerLibrary { + /** + * Color library + */ + private ColorLibrary colors; + + /** + * Color of "uncolored" dots + */ + private String dotcolor = "black"; + + /** + * Color of "greyed out" dots + */ + private String greycolor = "gray"; + + /** + * Constructor + * + * @param style Style library to use + */ + public CircleMarkers(StyleLibrary style) { + super(); + this.colors = style.getColorSet(StyleLibrary.MARKERPLOT); + this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT); + this.greycolor = style.getColor(StyleLibrary.PLOTGREY); + } + + /** + * Use a given marker on the document. + */ + @Override + public Element useMarker(SVGPlot plot, Element parent, double x, double y, int stylenr, double size) { + Element marker = plot.svgCircle(x, y, size * .5); + final String col; + if(stylenr == -1) { + col = dotcolor; + } + else if(stylenr == -2) { + col = greycolor; + } + else { + col = colors.getColor(stylenr); + } + SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + col); + parent.appendChild(marker); + return marker; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MarkerLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MarkerLibrary.java new file mode 100755 index 00000000..522097aa --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MarkerLibrary.java @@ -0,0 +1,56 @@ +package de.lmu.ifi.dbs.elki.visualization.style.marker; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; + +/** + * A marker library is a class that can generate and draw various styles of + * markers. Different uses might require different marker libraries (e.g. full + * screen, thumbnail, print) + * + * @author Erich Schubert + * + * @apiviz.uses Element oneway - - «create» + */ +public interface MarkerLibrary { + /** + * Insert a marker at the given coordinates. Markers will be defined in the + * defs part of the document, and then SVG-"use"d at the given coordinates. + * This supposedly is more efficient and significantly reduces file size. + * Symbols will be named "s0", "s1" etc.; these names must not be used by + * other elements in the SVG document! + * + * @param plot Plot to draw on + * @param parent parent node + * @param x coordinate + * @param y coordinate + * @param style style (enumerated) + * @param size size + * @return Element node generated. + */ + public Element useMarker(SVGPlot plot, Element parent, double x, double y, int style, double size); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MinimalMarkers.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MinimalMarkers.java new file mode 100755 index 00000000..a3390a6d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MinimalMarkers.java @@ -0,0 +1,90 @@ +package de.lmu.ifi.dbs.elki.visualization.style.marker; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Simple marker library that just draws colored rectangles at the given + * coordinates. + * + * @author Erich Schubert + * + * @apiviz.composedOf ColorLibrary + */ +public class MinimalMarkers implements MarkerLibrary { + /** + * Color library + */ + private ColorLibrary colors; + + /** + * Color of "uncolored" dots + */ + private String dotcolor = "black"; + + /** + * Color of "greyed out" dots + */ + private String greycolor = "gray"; + + /** + * Constructor + * + * @param style Style library to use + */ + public MinimalMarkers(StyleLibrary style) { + super(); + this.colors = style.getColorSet(StyleLibrary.MARKERPLOT); + this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT); + this.greycolor = style.getColor(StyleLibrary.PLOTGREY); + } + + /** + * Use a given marker on the document. + */ + @Override + public Element useMarker(SVGPlot plot, Element parent, double x, double y, int stylenr, double size) { + Element marker = plot.svgRect(x - size * .5, y - size * .5, size, size); + final String col; + if(stylenr == -1) { + col = dotcolor; + } + else if(stylenr == -2) { + col = greycolor; + } + else { + col = colors.getColor(stylenr); + } + SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + col); + parent.appendChild(marker); + return marker; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/PrettyMarkers.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/PrettyMarkers.java new file mode 100755 index 00000000..be99c6b1 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/PrettyMarkers.java @@ -0,0 +1,235 @@ +package de.lmu.ifi.dbs.elki.visualization.style.marker; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Marker library achieving a larger number of styles by combining different + * shapes with different colors. Uses object ID management by SVGPlot. + * + * @author Erich Schubert + * + * @apiviz.composedOf ColorLibrary + */ +public class PrettyMarkers implements MarkerLibrary { + /** + * Color library + */ + private ColorLibrary colors; + + /** + * Default prefix to use. + */ + private static final String DEFAULT_PREFIX = "s"; + + /** + * Prefix for the IDs generated. + */ + private String prefix; + + /** + * Color of "uncolored" dots + */ + private String dotcolor; + + /** + * Color of "greyed out" dots + */ + private String greycolor; + + /** + * Constructor + * + * @param prefix prefix to use. + * @param style style library to use + */ + public PrettyMarkers(String prefix, StyleLibrary style) { + this.prefix = prefix; + this.colors = style.getColorSet(StyleLibrary.MARKERPLOT); + this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT); + this.greycolor = style.getColor(StyleLibrary.PLOTGREY); + } + + /** + * Constructor without prefix argument, will use {@link #DEFAULT_PREFIX} as + * prefix. + * + * @param style Style library to use + */ + public PrettyMarkers(StyleLibrary style) { + this(DEFAULT_PREFIX, style); + } + + /** + * Draw an marker used in scatter plots. If you intend to use the markers + * multiple times, you should consider using the {@link #useMarker} method + * instead, which exploits the SVG features of symbol definition and use + * + * @param plot containing plot + * @param parent parent node + * @param x position + * @param y position + * @param style marker style (enumerated) + * @param size size + */ + public void plotMarker(SVGPlot plot, Element parent, double x, double y, int style, double size) { + assert (parent != null); + if (style == -1) { + plotUncolored(plot, parent, x, y, size); + return; + } + if (style == -2) { + plotGray(plot, parent, x, y, size); + return; + } + // TODO: add more styles. + String colorstr = colors.getColor(style); + String strokestyle = SVGConstants.CSS_STROKE_PROPERTY + ":" + colorstr + ";" + SVGConstants.CSS_STROKE_WIDTH_PROPERTY + ":" + SVGUtil.fmt(size / 6); + + switch(style % 8){ + case 0: { + // + cross + Element line1 = plot.svgLine(x, y - size / 2.2, x, y + size / 2.2); + SVGUtil.setStyle(line1, strokestyle); + parent.appendChild(line1); + Element line2 = plot.svgLine(x - size / 2.2, y, x + size / 2.2, y); + SVGUtil.setStyle(line2, strokestyle); + parent.appendChild(line2); + break; + } + case 1: { + // X cross + Element line1 = plot.svgLine(x - size / 2.828427, y - size / 2.828427, x + size / 2.828427, y + size / 2.828427); + SVGUtil.setStyle(line1, strokestyle); + parent.appendChild(line1); + Element line2 = plot.svgLine(x - size / 2.828427, y + size / 2.828427, x + size / 2.828427, y - size / 2.828427); + SVGUtil.setStyle(line2, strokestyle); + parent.appendChild(line2); + break; + } + case 2: { + // O hollow circle + Element circ = plot.svgCircle(x, y, size / 2.2); + SVGUtil.setStyle(circ, "fill: none;" + strokestyle); + parent.appendChild(circ); + break; + } + case 3: { + // [] hollow rectangle + Element rect = plot.svgRect(x - size / 2.4, y - size / 2.4, size / 1.2, size / 1.2); + SVGUtil.setStyle(rect, "fill: none;" + strokestyle); + parent.appendChild(rect); + break; + } + case 4: { + // <> hollow diamond + Element rect = plot.svgRect(x - size / 2.7, y - size / 2.7, size / 1.35, size / 1.35); + SVGUtil.setStyle(rect, "fill: none;" + strokestyle); + SVGUtil.setAtt(rect, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(45," + SVGUtil.fmt(x) + "," + SVGUtil.fmt(y) + ")"); + parent.appendChild(rect); + break; + } + case 5: { + // O filled circle + Element circ = plot.svgCircle(x, y, size * .5); + SVGUtil.setStyle(circ, SVGConstants.CSS_FILL_PROPERTY + ":" + colorstr); + parent.appendChild(circ); + break; + } + case 6: { + // [] filled rectangle + Element rect = plot.svgRect(x - size / 2.2, y - size / 2.2, size / 1.1, size / 1.1); + SVGUtil.setStyle(rect, "fill:" + colorstr); + parent.appendChild(rect); + break; + } + case 7: { + // <> filled diamond + Element rect = plot.svgRect(x - size / 2.5, y - size / 2.5, size / 1.25, size / 1.25); + SVGUtil.setStyle(rect, "fill:" + colorstr); + SVGUtil.setAtt(rect, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(45," + SVGUtil.fmt(x) + "," + SVGUtil.fmt(y) + ")"); + parent.appendChild(rect); + break; + } + } + } + + /** + * Plot a replacement marker when an object is to be plotted as "disabled", usually gray. + * + * @param plot Plot to draw to + * @param parent Parent element + * @param x X position + * @param y Y position + * @param size Size + */ + protected void plotGray(SVGPlot plot, Element parent, double x, double y, double size) { + Element marker = plot.svgCircle(x, y, size * .5); + SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + greycolor); + parent.appendChild(marker); + } + + /** + * Plot a replacement marker when no color is set; usually black + * + * @param plot Plot to draw to + * @param parent Parent element + * @param x X position + * @param y Y position + * @param size Size + */ + protected void plotUncolored(SVGPlot plot, Element parent, double x, double y, double size) { + Element marker = plot.svgCircle(x, y, size * .5); + SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + dotcolor); + parent.appendChild(marker); + } + + @Override + public Element useMarker(SVGPlot plot, Element parent, double x, double y, int style, double size) { + String id = prefix + style + "_" + size; + Element existing = plot.getIdElement(id); + if(existing == null) { + Element symbol = plot.svgElement(SVGConstants.SVG_SYMBOL_TAG); + SVGUtil.setAtt(symbol, SVGConstants.SVG_ID_ATTRIBUTE, id); + plotMarker(plot, symbol, 2 * size, 2 * size, style, 2 * size); + plot.getDefs().appendChild(symbol); + plot.putIdElement(id, symbol); + } + Element use = plot.svgElement(SVGConstants.SVG_USE_TAG); + use.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, "#" + id); + SVGUtil.setAtt(use, SVGConstants.SVG_X_ATTRIBUTE, x - 2 * size); + SVGUtil.setAtt(use, SVGConstants.SVG_Y_ATTRIBUTE, y - 2 * size); + if(parent != null) { + parent.appendChild(use); + } + return use; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/package-info.java new file mode 100755 index 00000000..293d5a67 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Draw plot markers</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.style.marker;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/package-info.java new file mode 100755 index 00000000..90c548cd --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Style management for ELKI visualizations.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.style;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGArrow.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGArrow.java new file mode 100644 index 00000000..afa42afd --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGArrow.java @@ -0,0 +1,114 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +/** + * Static class for drawing simple arrows + * + * @author Erich Schubert + * @author Robert Rödler + * + * @apiviz.uses SVGPath + */ +public final class SVGArrow { + /** + * Direction constants + * + * @author Erich Schubert + * @author Robert Rödler + * + * @apiviz.exclude + */ + public static enum Direction { + LEFT, DOWN, RIGHT, UP, // SWAPWITH, INSERT + } + + /** + * Constant for "up" + */ + public static final Direction UP = Direction.UP; + + /** + * Constant for "down" + */ + public static final Direction DOWN = Direction.DOWN; + + /** + * Constant for "right" + */ + public static final Direction RIGHT = Direction.RIGHT; + + /** + * Constant for "left" + */ + public static final Direction LEFT = Direction.LEFT; + + /** + * Draw an arrow at the given position. + * + * Note: the arrow is an unstyled svg path. You need to apply style afterwards. + * + * @param svgp Plot to draw to + * @param dir Direction to draw + * @param x Center x coordinate + * @param y Center y coordinate + * @param size Arrow size + * @return SVG Element + */ + public static Element makeArrow(SVGPlot svgp, Direction dir, double x, double y, double size) { + final SVGPath path = new SVGPath(); + final double hs = size / 2.; + + switch(dir){ + case LEFT: + path.drawTo(x + hs, y + hs); + path.drawTo(x - hs, y); + path.drawTo(x + hs, y - hs); + path.drawTo(x + hs, y + hs); + break; + case DOWN: + path.drawTo(x - hs, y - hs); + path.drawTo(x + hs, y - hs); + path.drawTo(x, y + hs); + path.drawTo(x - hs, y - hs); + break; + case RIGHT: + path.drawTo(x - hs, y - hs); + path.drawTo(x + hs, y); + path.drawTo(x - hs, y + hs); + path.drawTo(x - hs, y - hs); + break; + case UP: + path.drawTo(x - hs, y + hs); + path.drawTo(x, y - hs); + path.drawTo(x + hs, y + hs); + path.drawTo(x - hs, y + hs); + break; + } + path.close(); + return path.makeElement(svgp); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java new file mode 100644 index 00000000..6a542a7e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java @@ -0,0 +1,165 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; + +/** + * Class to draw a button as SVG. + * + * @author Erich Schubert + */ +public class SVGButton { + /** + * Default button color + */ + public static final String DEFAULT_BUTTON_COLOR = SVGConstants.CSS_LIGHTGRAY_VALUE; + + /** + * Default text color + */ + public static final String DEFAULT_TEXT_COLOR = SVGConstants.CSS_BLACK_VALUE; + + /** + * X position + */ + private double x; + + /** + * Y position + */ + private double y; + + /** + * Width + */ + private double w; + + /** + * Height + */ + private double h; + + /** + * Corner rounding factor. NaN = no rounding + */ + private double r = Double.NaN; + + /** + * Class for the buttons main CSS + */ + private CSSClass butcss; + + /** + * Button title, optional + */ + private String title = null; + + /** + * Title styling + */ + private CSSClass titlecss = null; + + /** + * Constructor. + * + * @param x Position X + * @param y Position Y + * @param w Width + * @param h Height + * @param r Rounded radius + */ + public SVGButton(double x, double y, double w, double h, double r) { + super(); + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.r = r; + this.butcss = new CSSClass(this, "button"); + butcss.setStatement(SVGConstants.CSS_FILL_PROPERTY, DEFAULT_BUTTON_COLOR); + butcss.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + butcss.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, ".01"); + } + + /** + * Produce the actual SVG elements for the button. + * + * @param svgp Plot to draw to + * @return Button wrapper element + */ + public Element render(SVGPlot svgp) { + Element tag = svgp.svgElement(SVGConstants.SVG_G_TAG); + Element button = svgp.svgRect(x, y, w, h); + if(!Double.isNaN(r)) { + SVGUtil.setAtt(button, SVGConstants.SVG_RX_ATTRIBUTE, r); + SVGUtil.setAtt(button, SVGConstants.SVG_RY_ATTRIBUTE, r); + } + SVGUtil.setAtt(button, SVGConstants.SVG_STYLE_ATTRIBUTE, butcss.inlineCSS()); + tag.appendChild(button); + // Add light effect: + if (svgp.getIdElement(SVGEffects.LIGHT_GRADIENT_ID) != null) { + Element light = svgp.svgRect(x, y, w, h); + if(!Double.isNaN(r)) { + SVGUtil.setAtt(light, SVGConstants.SVG_RX_ATTRIBUTE, r); + SVGUtil.setAtt(light, SVGConstants.SVG_RY_ATTRIBUTE, r); + } + SVGUtil.setAtt(light, SVGConstants.SVG_STYLE_ATTRIBUTE, "fill:url(#"+SVGEffects.LIGHT_GRADIENT_ID+");fill-opacity:.5"); + tag.appendChild(light); + } + + // Add shadow effect: + if(svgp.getIdElement(SVGEffects.SHADOW_ID) != null) { + //Element shadow = svgp.svgRect(x + (w * .05), y + (h * .05), w, h); + //SVGUtil.setAtt(button, SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_FILL_PROPERTY + ":" + SVGConstants.CSS_BLACK_VALUE); + button.setAttribute(SVGConstants.SVG_FILTER_ATTRIBUTE, "url(#" + SVGEffects.SHADOW_ID + ")"); + //tag.appendChild(shadow); + } + + if(title != null) { + Element label = svgp.svgText(x + w * .5, y + h * .7, title); + label.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, titlecss.inlineCSS()); + tag.appendChild(label); + } + return tag; + } + + /** + * Set the button title + * + * @param title Button title + * @param textcolor Color + */ + public void setTitle(String title, String textcolor) { + this.title = title; + if(titlecss == null) { + titlecss = new CSSClass(this, "text"); + titlecss.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.CSS_MIDDLE_VALUE); + titlecss.setStatement(SVGConstants.CSS_FILL_PROPERTY, textcolor); + titlecss.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, .6 * h); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java new file mode 100644 index 00000000..1cab95b9 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java @@ -0,0 +1,187 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.EventListenerList; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +/** + * SVG checkbox. + * + * @author Sascha Goldhofer + */ +public class SVGCheckbox { + /** + * Status flag + */ + protected boolean checked; + + /** + * Event listeners + */ + protected EventListenerList listenerList = new EventListenerList(); + + /** + * Checkbox label + */ + protected String label = null; + + /** + * Constructor, without label + * + * @param checked Checked status + */ + public SVGCheckbox(boolean checked) { + this(checked, null); + } + + /** + * Constructor, with label + * + * @param checked Checked status + * @param label Label + */ + public SVGCheckbox(boolean checked, String label) { + this.checked = checked; + this.label = label; + } + + /** + * Render the SVG checkbox to a plot + * + * @param svgp Plot to draw to + * @param x X offset + * @param y Y offset + * @param size Size factor + * @return Container element + */ + public Element renderCheckBox(SVGPlot svgp, double x, double y, double size) { + // create check + final Element checkmark = SVGEffects.makeCheckmark(svgp); + checkmark.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "scale(" + (size / 11) + ") translate(" + x + " " + y + ")"); + if(!checked) { + checkmark.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_DISPLAY_PROPERTY + ":" + SVGConstants.CSS_NONE_VALUE); + } + + // create box + Element checkbox_box = SVGUtil.svgRect(svgp.getDocument(), x, y, size, size); + checkbox_box.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1"); + checkbox_box.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0"); + checkbox_box.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "0.5"); + + // create checkbox + final Element checkbox = svgp.svgElement(SVGConstants.SVG_G_TAG); + checkbox.appendChild(checkbox_box); + checkbox.appendChild(checkmark); + + // create Label + if(label != null) { + Element labele = svgp.svgText(x + 2 * size, y + size, label); + // TODO: font size! + checkbox.appendChild(labele); + } + + // add click event listener + EventTarget targ = (EventTarget) checkbox; + targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, new EventListener() { + /** + * Set the checkmark, and fire the event. + */ + protected void check() { + checkmark.removeAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE); + checked = true; + fireSwitchEvent(new ChangeEvent(SVGCheckbox.this)); + } + + /** + * Remove the checkmark and fire the event. + */ + protected void uncheck() { + checkmark.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_DISPLAY_PROPERTY + ":" + SVGConstants.CSS_NONE_VALUE); + checked = false; + fireSwitchEvent(new ChangeEvent(SVGCheckbox.this)); + } + + @Override + public void handleEvent(Event evt) { + if(checked) { + uncheck(); + } + else { + check(); + } + } + }, false); + + return checkbox; + } + + /** + * Register a listener for this checkbox. + * + * @param listener Listener to add + */ + public void addCheckBoxListener(ChangeListener listener) { + listenerList.add(ChangeListener.class, listener); + } + + /** + * Remove a listener for this checkbox. + * + * @param listener Listener to remove + */ + public void removeCheckBoxListener(ChangeListener listener) { + listenerList.remove(ChangeListener.class, listener); + } + + /** + * Return the checkmark status. + * + * @return true, when checked + */ + public boolean isChecked() { + return checked; + } + + /** + * Fire the event to listeners + * + * @param evt Event to fire + */ + protected void fireSwitchEvent(ChangeEvent evt) { + Object[] listeners = listenerList.getListenerList(); + for(int i = 0; i < listeners.length; i += 2) { + if(listeners[i] == ChangeListener.class) { + ((ChangeListener) listeners[i + 1]).stateChanged(evt); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java new file mode 100644 index 00000000..c09717f7 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java @@ -0,0 +1,60 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import de.lmu.ifi.dbs.elki.utilities.xml.DOMCloner; + +/** + * Clone visible parts of an SVG document. + * + * @author Erich Schubert + */ +public class SVGCloneVisible extends DOMCloner { + @Override + public Node cloneNode(Document doc, Node eold) { + // Skip elements with visibility=hidden + if(eold instanceof Element) { + Element eeold = (Element) eold; + String vis = eeold.getAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY); + if(SVGConstants.CSS_HIDDEN_VALUE.equals(vis)) { + return null; + } + } + // Perform clone flat + Node enew = doc.importNode(eold, false); + // Recurse: + for(Node n = eold.getFirstChild(); n != null; n = n.getNextSibling()) { + final Node clone = cloneNode(doc, n); + if (clone != null) { + enew.appendChild(clone); + } + } + return enew; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java new file mode 100644 index 00000000..4171ce51 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java @@ -0,0 +1,147 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +/** + * Class containing some popular SVG effects. + * + * @author Erich Schubert + */ +public class SVGEffects { + /** + * ID for the drop shadow effect + */ + public static final String SHADOW_ID = "shadow-effect"; + + /** + * ID for the light gradient fill + */ + public static final String LIGHT_GRADIENT_ID = "light-gradient"; + + /** + * Static method to prepare a SVG document for drop shadow effects. + * + * Invoke this from an appropriate update thread! + * + * @param svgp Plot to prepare + */ + public static void addShadowFilter(SVGPlot svgp) { + Element shadow = svgp.getIdElement(SHADOW_ID); + if(shadow == null) { + shadow = svgp.svgElement(SVGConstants.SVG_FILTER_TAG); + shadow.setAttribute(SVGConstants.SVG_ID_ATTRIBUTE, SHADOW_ID); + shadow.setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "140%"); + shadow.setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "140%"); + + Element offset = svgp.svgElement(SVGConstants.SVG_FE_OFFSET_TAG); + offset.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, SVGConstants.SVG_SOURCE_ALPHA_VALUE); + offset.setAttribute(SVGConstants.SVG_RESULT_ATTRIBUTE, "off"); + offset.setAttribute(SVGConstants.SVG_DX_ATTRIBUTE, "0.1"); + offset.setAttribute(SVGConstants.SVG_DY_ATTRIBUTE, "0.1"); + shadow.appendChild(offset); + + Element gauss = svgp.svgElement(SVGConstants.SVG_FE_GAUSSIAN_BLUR_TAG); + gauss.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, "off"); + gauss.setAttribute(SVGConstants.SVG_RESULT_ATTRIBUTE, "blur"); + gauss.setAttribute(SVGConstants.SVG_STD_DEVIATION_ATTRIBUTE, "0.1"); + shadow.appendChild(gauss); + + Element blend = svgp.svgElement(SVGConstants.SVG_FE_BLEND_TAG); + blend.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, SVGConstants.SVG_SOURCE_GRAPHIC_VALUE); + blend.setAttribute(SVGConstants.SVG_IN2_ATTRIBUTE, "blur"); + blend.setAttribute(SVGConstants.SVG_MODE_ATTRIBUTE, SVGConstants.SVG_NORMAL_VALUE); + shadow.appendChild(blend); + + svgp.getDefs().appendChild(shadow); + svgp.putIdElement(SHADOW_ID, shadow); + } + } + + /** + * Static method to prepare a SVG document for light gradient effects. + * + * Invoke this from an appropriate update thread! + * + * @param svgp Plot to prepare + */ + public static void addLightGradient(SVGPlot svgp) { + Element gradient = svgp.getIdElement(LIGHT_GRADIENT_ID); + if(gradient == null) { + gradient = svgp.svgElement(SVGConstants.SVG_LINEAR_GRADIENT_TAG); + gradient.setAttribute(SVGConstants.SVG_ID_ATTRIBUTE, LIGHT_GRADIENT_ID); + gradient.setAttribute(SVGConstants.SVG_X1_ATTRIBUTE, "0"); + gradient.setAttribute(SVGConstants.SVG_Y1_ATTRIBUTE, "0"); + gradient.setAttribute(SVGConstants.SVG_X2_ATTRIBUTE, "0"); + gradient.setAttribute(SVGConstants.SVG_Y2_ATTRIBUTE, "1"); + + Element stop0 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop0.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "white"); + stop0.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "1"); + stop0.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, "0"); + gradient.appendChild(stop0); + + Element stop04 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop04.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "white"); + stop04.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "0"); + stop04.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, ".4"); + gradient.appendChild(stop04); + + Element stop06 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop06.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "black"); + stop06.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "0"); + stop06.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, ".6"); + gradient.appendChild(stop06); + + Element stop1 = svgp.svgElement(SVGConstants.SVG_STOP_TAG); + stop1.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "black"); + stop1.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, ".5"); + stop1.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, "1"); + gradient.appendChild(stop1); + + svgp.getDefs().appendChild(gradient); + svgp.putIdElement(LIGHT_GRADIENT_ID, gradient); + } + } + + /** + * Checkmark path, sized approx. 15x15 + */ + public static final String SVG_CHECKMARK_PATH = "M0 6.458 L2.047 4.426 5.442 7.721 12.795 0 15 2.117 5.66 11.922 Z"; + + /** + * Creates a 15x15 big checkmark + * + * @param svgp Plot to create the element for + * @return Element + */ + public static Element makeCheckmark(SVGPlot svgp) { + Element checkmark = svgp.svgElement(SVGConstants.SVG_PATH_TAG); + checkmark.setAttribute(SVGConstants.SVG_D_ATTRIBUTE, SVG_CHECKMARK_PATH); + checkmark.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.CSS_BLACK_VALUE); + checkmark.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.CSS_NONE_VALUE); + return checkmark; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java new file mode 100644 index 00000000..9faad348 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java @@ -0,0 +1,340 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.List; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable; +import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath; +import de.lmu.ifi.dbs.elki.utilities.BitsUtil; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; + +/** + * Utility class to draw hypercubes, wireframe and filled. + * + * @author Erich Schubert + * + * @apiviz.uses SVGPath + * @apiviz.uses Projection2D + */ +public class SVGHyperCube { + /** + * Wireframe hypercube. + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param min First corner + * @param max Opposite corner + * @return path element + */ + public static Element drawFrame(SVGPlot svgp, Projection2D proj, double[] min, double[] max) { + SVGPath path = new SVGPath(); + ArrayList<double[]> edges = getVisibleEdges(proj, min, max); + double[] rv_min = proj.fastProjectDataToRenderSpace(min); + recDrawEdges(path, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size())); + return path.makeElement(svgp); + } + + /** + * Wireframe hypercube. + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param min First corner + * @param max Opposite corner + * @return path element + */ + public static Element drawFrame(SVGPlot svgp, Projection2D proj, NumberVector min, NumberVector max) { + SVGPath path = new SVGPath(); + ArrayList<double[]> edges = getVisibleEdges(proj, min, max); + double[] rv_min = proj.fastProjectDataToRenderSpace(min); + recDrawEdges(path, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size())); + return path.makeElement(svgp); + } + + /** + * Wireframe hypercube. + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param box Bounding box + * @return path element + */ + public static Element drawFrame(SVGPlot svgp, Projection2D proj, SpatialComparable box) { + SVGPath path = new SVGPath(); + ArrayList<double[]> edges = getVisibleEdges(proj, box); + final int dim = box.getDimensionality(); + double[] min = new double[dim]; + for(int i = 0; i < dim; i++) { + min[i] = box.getMin(i); + } + double[] rv_min = proj.fastProjectDataToRenderSpace(min); + recDrawEdges(path, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size())); + return path.makeElement(svgp); + } + + /** + * Filled hypercube. + * + * @param svgp SVG Plot + * @param cls CSS class to use. + * @param proj Visualization projection + * @param min First corner + * @param max Opposite corner + * @return group element + */ + public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, double[] min, double[] max) { + Element group = svgp.svgElement(SVGConstants.SVG_G_TAG); + ArrayList<double[]> edges = getVisibleEdges(proj, min, max); + double[] rv_min = proj.fastProjectDataToRenderSpace(min); + recDrawSides(svgp, group, cls, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size())); + return group; + } + + /** + * Filled hypercube. + * + * @param svgp SVG Plot + * @param cls CSS class to use. + * @param proj Visualization projection + * @param min First corner + * @param max Opposite corner + * @return group element + */ + public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, NumberVector min, NumberVector max) { + Element group = svgp.svgElement(SVGConstants.SVG_G_TAG); + ArrayList<double[]> edges = getVisibleEdges(proj, min, max); + double[] rv_min = proj.fastProjectDataToRenderSpace(min); + recDrawSides(svgp, group, cls, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size())); + return group; + } + + /** + * Filled hypercube. + * + * @param svgp SVG Plot + * @param cls CSS class to use. + * @param proj Visualization projection + * @param box Bounding box + * @return group element + */ + public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, SpatialComparable box) { + Element group = svgp.svgElement(SVGConstants.SVG_G_TAG); + ArrayList<double[]> edges = getVisibleEdges(proj, box); + final int dim = box.getDimensionality(); + double[] min = new double[dim]; + for(int i = 0; i < dim; i++) { + min[i] = box.getMin(i); + } + double[] rv_min = proj.fastProjectDataToRenderSpace(min); + recDrawSides(svgp, group, cls, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size())); + return group; + } + + /** + * Get the visible (non-0) edges of a hypercube + * + * @param proj Projection + * @param s_min Minimum value (in data space) + * @param s_max Maximum value (in data space) + * @return Edge list + */ + private static ArrayList<double[]> getVisibleEdges(Projection2D proj, double[] s_min, double[] s_max) { + final int dim = s_min.length; + double[] s_deltas = VMath.minus(s_max, s_min); + ArrayList<double[]> r_edges = new ArrayList<>(dim); + for(int i = 0; i < dim; i++) { + double[] delta = new double[dim]; + delta[i] = s_deltas[i]; + double[] deltas = proj.fastProjectRelativeDataToRenderSpace(delta); + if(deltas[0] != 0 || deltas[1] != 0) { + r_edges.add(deltas); + } + } + return r_edges; + } + + /** + * Get the visible (non-0) edges of a hypercube + * + * @param proj Projection + * @param s_min Minimum value (in data space) + * @param s_max Maximum value (in data space) + * @return Edge list + */ + private static ArrayList<double[]> getVisibleEdges(Projection2D proj, NumberVector s_min, NumberVector s_max) { + final int dim = s_min.getDimensionality(); + double[] s_deltas = new double[dim]; + for(int i = 0; i < dim; i++) { + s_deltas[i] = s_max.doubleValue(i) - s_min.doubleValue(i); + } + ArrayList<double[]> r_edges = new ArrayList<>(dim); + for(int i = 0; i < dim; i++) { + double[] delta = new double[dim]; + delta[i] = s_deltas[i]; + double[] deltas = proj.fastProjectRelativeDataToRenderSpace(delta); + if(deltas[0] != 0 || deltas[1] != 0) { + r_edges.add(deltas); + } + } + return r_edges; + } + + /** + * Get the visible (non-0) edges of a hypercube + * + * @param proj Projection + * @param box Box object + * @return Edge list + */ + private static ArrayList<double[]> getVisibleEdges(Projection2D proj, SpatialComparable box) { + final int dim = box.getDimensionality(); + double[] s_deltas = new double[dim]; + for(int i = 0; i < dim; i++) { + s_deltas[i] = box.getMax(i) - box.getMin(i); + } + ArrayList<double[]> r_edges = new ArrayList<>(dim); + for(int i = 0; i < dim; i++) { + double[] delta = new double[dim]; + delta[i] = s_deltas[i]; + double[] deltas = proj.fastProjectRelativeDataToRenderSpace(delta); + if(deltas[0] != 0 || deltas[1] != 0) { + r_edges.add(deltas); + } + } + return r_edges; + } + + /** + * Recursive helper for hypercube drawing. + * + * @param path path + * @param minx starting corner + * @param miny starting corner + * @param r_edges edge vectors + * @param off recursion offset (to avoid multi-recursion) + * @param b bit set of drawn edges + */ + private static void recDrawEdges(SVGPath path, double minx, double miny, List<double[]> r_edges, int off, long[] b) { + // Draw all "missing" edges + for(int i = 0; i < r_edges.size(); i++) { + if(BitsUtil.get(b, i)) { + continue; + } + final double[] edge = r_edges.get(i); + final double x_i = minx + edge[0]; + if(!isFinite(x_i)) { + continue; + } + final double y_i = miny + edge[1]; + if(!isFinite(y_i)) { + continue; + } + path.moveTo(minx, miny); + path.drawTo(x_i, y_i); + // Recursion + BitsUtil.setI(b, i); + recDrawEdges(path, x_i, y_i , r_edges, i + 1, b); + BitsUtil.clearI(b, i); + } + } + + /** + * Recursive helper for hypercube drawing. + * + * @param plot Plot + * @param group Group element + * @param cls CSS class + * @param minx starting corner + * @param miny starting corner + * @param r_edges edge vectors + * @param off recursion offset (to avoid multi-recursion) + * @param b bit set of drawn edges + */ + private static void recDrawSides(SVGPlot plot, Element group, String cls, double minx, double miny, List<double[]> r_edges, int off, long[] b) { + StringBuilder pbuf = new StringBuilder(); + // Draw all "missing" sides + for(int i = 0; i < r_edges.size() - 1; i++) { + if(BitsUtil.get(b, i)) { + continue; + } + double[] deltai = r_edges.get(i); + final double xi = minx + deltai[0]; + if(!isFinite(xi)) { + continue; + } + final double yi = miny + deltai[1]; + if(!isFinite(yi)) { + continue; + } + for(int j = i + 1; j < r_edges.size(); j++) { + if(BitsUtil.get(b, j)) { + continue; + } + double[] deltaj = r_edges.get(j); + final double dxj = deltaj[0]; + if(!isFinite(xi)) { + continue; + } + final double dyj = deltaj[1]; + if(!isFinite(dxj)) { + continue; + } + pbuf.delete(0, pbuf.length()); // Clear + pbuf.append(SVGUtil.fmt(minx)).append(','); + pbuf.append(SVGUtil.fmt(miny)).append(' '); + pbuf.append(SVGUtil.fmt(xi)).append(','); + pbuf.append(SVGUtil.fmt(yi)).append(' '); + pbuf.append(SVGUtil.fmt(xi + dxj)).append(','); + pbuf.append(SVGUtil.fmt(yi + dyj)).append(' '); + pbuf.append(SVGUtil.fmt(minx + dxj)).append(','); + pbuf.append(SVGUtil.fmt(miny + dyj)); + + Element poly = plot.svgElement(SVGConstants.SVG_POLYGON_TAG); + SVGUtil.setAtt(poly, SVGConstants.SVG_POINTS_ATTRIBUTE, pbuf.toString()); + SVGUtil.setCSSClass(poly, cls); + group.appendChild(poly); + } + // Recursion + BitsUtil.setI(b, i); + recDrawSides(plot, group, cls, xi, yi, r_edges, i + 1, b); + BitsUtil.clearI(b, i); + } + } + + /** + * Finite (and not NaN) double values. + * + * @param v Value + * @return true, when finite. + */ + private static boolean isFinite(double v) { + return v < Double.POSITIVE_INFINITY && v > Double.NEGATIVE_INFINITY; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java new file mode 100644 index 00000000..e3fc9386 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java @@ -0,0 +1,289 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.utilities.BitsUtil; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; + +/** + * Utility class to draw hypercubes, wireframe and filled. + * + * @author Erich Schubert + * + * @apiviz.uses SVGPath + * @apiviz.uses Projection2D + */ +public class SVGHyperSphere { + /** + * Factor used for approximating circles with cubic beziers. + * + * kappa = 4 * (Math.sqrt(2)-1)/3 + */ + public static final double EUCLIDEAN_KAPPA = 0.5522847498; + + /** + * Wireframe "manhattan" hypersphere + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param mid mean vector + * @param radius radius + * @return path element + */ + public static Element drawManhattan(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius) { + final double[] v_mid = mid.getColumnVector().getArrayRef(); // a copy + final long[] dims = proj.getVisibleDimensions2D(); + + SVGPath path = new SVGPath(); + for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) { + v_mid[dim] += radius; + double[] p1 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; + v_mid[dim] -= radius; + double[] p2 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; + for(int dim2 = BitsUtil.nextSetBit(dims, 0); dim2 >= 0; dim2 = BitsUtil.nextSetBit(dims, dim2 + 1)) { + if(dim < dim2) { + v_mid[dim2] += radius; + double[] p3 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] -= radius; + v_mid[dim2] -= radius; + double[] p4 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] += radius; + + path.moveTo(p1[0], p1[1]); + path.drawTo(p3[0], p3[1]); + path.moveTo(p1[0], p1[1]); + path.drawTo(p4[0], p4[1]); + path.moveTo(p2[0], p2[1]); + path.drawTo(p3[0], p3[1]); + path.moveTo(p2[0], p2[1]); + path.drawTo(p4[0], p4[1]); + path.close(); + } + } + } + return path.makeElement(svgp); + } + + /** + * Wireframe "euclidean" hypersphere + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param mid mean vector + * @param radius radius + * @return path element + */ + public static Element drawEuclidean(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius) { + double[] v_mid = mid.getColumnVector().getArrayRef(); // a copy + long[] dims = proj.getVisibleDimensions2D(); + + SVGPath path = new SVGPath(); + for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) { + v_mid[dim] += radius; + double[] p1 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; + v_mid[dim] -= radius; + double[] p2 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; + // delta vector + double[] dt1 = new double[v_mid.length]; + dt1[dim] = radius; + double[] d1 = proj.fastProjectRelativeDataToRenderSpace(dt1); + for(int dim2 = BitsUtil.nextSetBit(dims, 0); dim2 >= 0; dim2 = BitsUtil.nextSetBit(dims, dim2 + 1)) { + if(dim < dim2) { + v_mid[dim2] += radius; + double[] p3 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] -= radius; + v_mid[dim2] -= radius; + double[] p4 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] += radius; + // delta vector + double[] dt2 = new double[v_mid.length]; + dt2[dim2] = radius; + double[] d2 = proj.fastProjectRelativeDataToRenderSpace(dt2); + + path.moveTo(p1[0], p1[1]); + path.cubicTo(p1[0] + d2[0] * EUCLIDEAN_KAPPA, p1[1] + d2[1] * EUCLIDEAN_KAPPA, p3[0] + d1[0] * EUCLIDEAN_KAPPA, p3[1] + d1[1] * EUCLIDEAN_KAPPA, p3[0], p3[1]); + path.cubicTo(p3[0] - d1[0] * EUCLIDEAN_KAPPA, p3[1] - d1[1] * EUCLIDEAN_KAPPA, p2[0] + d2[0] * EUCLIDEAN_KAPPA, p2[1] + d2[1] * EUCLIDEAN_KAPPA, p2[0], p2[1]); + path.cubicTo(p2[0] - d2[0] * EUCLIDEAN_KAPPA, p2[1] - d2[1] * EUCLIDEAN_KAPPA, p4[0] - d1[0] * EUCLIDEAN_KAPPA, p4[1] - d1[1] * EUCLIDEAN_KAPPA, p4[0], p4[1]); + path.cubicTo(p4[0] + d1[0] * EUCLIDEAN_KAPPA, p4[1] + d1[1] * EUCLIDEAN_KAPPA, p1[0] - d2[0] * EUCLIDEAN_KAPPA, p1[1] - d2[1] * EUCLIDEAN_KAPPA, p1[0], p1[1]); + path.close(); + } + } + } + return path.makeElement(svgp); + } + + /** + * Wireframe "Lp" hypersphere + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param mid mean vector + * @param radius radius + * @param p L_p value + * @return path element + */ + public static Element drawLp(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius, double p) { + final double[] v_mid = mid.getColumnVector().getArrayRef(); + final long[] dims = proj.getVisibleDimensions2D(); + + final double kappax, kappay; + if(p > 1.) { + final double kappal = Math.pow(0.5, 1. / p); + kappax = Math.min(1.3, 4. * (2 * kappal - 1) / 3.); + kappay = 0; + } + else if(p < 1.) { + final double kappal = 1 - Math.pow(0.5, 1. / p); + kappax = 0; + kappay = Math.min(1.3, 4. * (2 * kappal - 1) / 3.); + } + else { + kappax = 0; + kappay = 0; + } + // LoggingUtil.warning("kappax: " + kappax + " kappay: " + kappay); + + SVGPath path = new SVGPath(); + for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) { + v_mid[dim] += radius; + double[] pvp0 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; + v_mid[dim] -= radius; + double[] pvm0 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; + // delta vector + double[] tvd0 = new double[v_mid.length]; + tvd0[dim] = radius; + double[] vd0 = proj.fastProjectRelativeDataToRenderSpace(tvd0); + for(int dim2 = BitsUtil.nextSetBit(dims, 0); dim2 >= 0; dim2 = BitsUtil.nextSetBit(dims, dim2 + 1)) { + if(dim < dim2) { + v_mid[dim2] += radius; + double[] pv0p = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] -= radius; + v_mid[dim2] -= radius; + double[] pv0m = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim2] += radius; + // delta vector + double[] tv0d = new double[v_mid.length]; + tv0d[dim2] = radius; + double[] v0d = proj.fastProjectRelativeDataToRenderSpace(tv0d); + + if(p > 1) { + // p > 1 + path.moveTo(pvp0[0], pvp0[1]); + // support points, p0 to 0p + final double s_pp1_x = pvp0[0] + v0d[0] * kappax; + final double s_pp1_y = pvp0[1] + v0d[1] * kappax; + final double s_pp2_x = pv0p[0] + vd0[0] * kappax; + final double s_pp2_y = pv0p[1] + vd0[1] * kappax; + path.cubicTo(s_pp1_x, s_pp1_y, s_pp2_x, s_pp2_y, pv0p[0], pv0p[1]); + // support points, 0p to m0 + final double s_mp1_x = pv0p[0] - vd0[0] * kappax; + final double s_mp1_y = pv0p[1] - vd0[1] * kappax; + final double s_mp2_x = pvm0[0] + v0d[0] * kappax; + final double s_mp2_y = pvm0[1] + v0d[1] * kappax; + path.cubicTo(s_mp1_x, s_mp1_y, s_mp2_x, s_mp2_y, pvm0[0], pvm0[1]); + // support points, m0 to 0m + final double s_mm1_x = pvm0[0] - v0d[0] * kappax; + final double s_mm1_y = pvm0[1] - v0d[1] * kappax; + final double s_mm2_x = pv0m[0] - vd0[0] * kappax; + final double s_mm2_y = pv0m[1] - vd0[1] * kappax; + path.cubicTo(s_mm1_x, s_mm1_y, s_mm2_x, s_mm2_y, pv0m[0], pv0m[1]); + // support points, 0m to p0 + final double s_pm1_x = pv0m[0] + vd0[0] * kappax; + final double s_pm1_y = pv0m[1] + vd0[1] * kappax; + final double s_pm2_x = pvp0[0] - v0d[0] * kappax; + final double s_pm2_y = pvp0[1] - v0d[1] * kappax; + path.cubicTo(s_pm1_x, s_pm1_y, s_pm2_x, s_pm2_y, pvp0[0], pvp0[1]); + path.close(); + } + else if(p < 1) { + // p < 1 + // support points, p0 to 0p + final double s_vp0_x = pvp0[0] - vd0[0] * kappay; + final double s_vp0_y = pvp0[1] - vd0[1] * kappay; + final double s_v0p_x = pv0p[0] - v0d[0] * kappay; + final double s_v0p_y = pv0p[1] - v0d[1] * kappay; + final double s_vm0_x = pvm0[0] + vd0[0] * kappay; + final double s_vm0_y = pvm0[1] + vd0[1] * kappay; + final double s_v0m_x = pv0m[0] + v0d[0] * kappay; + final double s_v0m_y = pv0m[1] + v0d[1] * kappay; + // Draw the star + path.moveTo(pvp0[0], pvp0[1]); + path.cubicTo(s_vp0_x, s_vp0_y, s_v0p_x, s_v0p_y, pv0p[0], pv0p[1]); + path.cubicTo(s_v0p_x, s_v0p_y, s_vm0_x, s_vm0_y, pvm0[0], pvm0[1]); + path.cubicTo(s_vm0_x, s_vm0_y, s_v0m_x, s_v0m_y, pv0m[0], pv0m[1]); + path.cubicTo(s_v0m_x, s_v0m_y, s_vp0_x, s_vp0_y, pvp0[0], pvp0[1]); + path.close(); + } + else { + // p == 1 - Manhattan + path.moveTo(pvp0[0], pvp0[1]); + path.lineTo(pv0p[0], pv0p[1]); + path.lineTo(pvm0[0], pvm0[1]); + path.lineTo(pv0m[0], pv0m[1]); + path.lineTo(pvp0[0], pvp0[1]); + path.close(); + } + } + } + } + return path.makeElement(svgp); + } + + /** + * Wireframe "cross" hypersphere + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param mid mean vector + * @param radius radius + * @return path element + */ + public static Element drawCross(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius) { + final double[] v_mid = mid.getColumnVector().getArrayRef(); + final long[] dims = proj.getVisibleDimensions2D(); + + SVGPath path = new SVGPath(); + for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) { + v_mid[dim] += radius; + double[] p1 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] -= radius; + path.moveTo(p1[0], p1[1]); + v_mid[dim] -= radius; + double[] p2 = proj.fastProjectDataToRenderSpace(v_mid); + v_mid[dim] += radius; + path.drawTo(p2[0], p2[1]); + path.close(); + } + return path.makeElement(svgp); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java new file mode 100644 index 00000000..948f09a7 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java @@ -0,0 +1,842 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.spatial.Polygon; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.ArrayListIter; + +/** + * Element used for building an SVG path using a string buffer. + * + * @author Erich Schubert + * + * @apiviz.uses Element oneway - - «create» + */ +public class SVGPath { + /** + * String buffer for building the path. + */ + private StringBuilder buf = new StringBuilder(); + + /** + * The last action we did, to not add unnecessary commands + */ + private String lastaction = null; + + /** + * The absolute "smooth cubic to" SVG path command (missing from + * SVGConstants). + */ + public static final String PATH_SMOOTH_CUBIC_TO = "S"; + + /** + * The lower case version (relative) line to command. + */ + public static final String PATH_LINE_TO_RELATIVE = SVGConstants.PATH_LINE_TO.toLowerCase(); + + /** + * The lower case version (relative) move command. + */ + public static final String PATH_MOVE_RELATIVE = SVGConstants.PATH_MOVE.toLowerCase(); + + /** + * The lower case version (relative) horizontal line to command. + */ + public static final String PATH_HORIZONTAL_LINE_TO_RELATIVE = SVGConstants.PATH_HORIZONTAL_LINE_TO.toLowerCase(); + + /** + * The lower case version (relative) vertical line to command. + */ + public static final String PATH_VERTICAL_LINE_TO_RELATIVE = SVGConstants.PATH_VERTICAL_LINE_TO.toLowerCase(); + + /** + * The lower case version (relative) cubic line to command. + */ + public static final String PATH_CUBIC_TO_RELATIVE = SVGConstants.PATH_CUBIC_TO.toLowerCase(); + + /** + * The lower case version (relative) smooth cubic to command. + */ + public static final String PATH_SMOOTH_CUBIC_TO_RELATIVE = PATH_SMOOTH_CUBIC_TO.toLowerCase(); + + /** + * The lower case version (relative) quadratic interpolation to command. + */ + public static final String PATH_QUAD_TO_RELATIVE = SVGConstants.PATH_QUAD_TO.toLowerCase(); + + /** + * The lower case version (relative) smooth quadratic interpolation to + * command. + */ + public static final String PATH_SMOOTH_QUAD_TO_RELATIVE = SVGConstants.PATH_SMOOTH_QUAD_TO.toLowerCase(); + + /** + * The lower case version (relative) path arc command. + */ + public static final String PATH_ARC_RELATIVE = SVGConstants.PATH_ARC.toLowerCase(); + + /** + * Empty path constructor. + */ + public SVGPath() { + // Nothing to do. + } + + /** + * Constructor with initial point. + * + * @param x initial coordinates + * @param y initial coordinates + */ + public SVGPath(double x, double y) { + this(); + this.moveTo(x, y); + } + + /** + * Constructor with initial point. + * + * @param xy initial coordinates + */ + public SVGPath(double[] xy) { + this(); + this.moveTo(xy[0], xy[1]); + } + + /** + * Constructor from a vector collection (e.g. a polygon) + * + * @param vectors vectors + */ + public SVGPath(Polygon vectors) { + this(); + for(ArrayListIter<Vector> it = vectors.iter(); it.valid(); it.advance()) { + Vector vec = it.get(); + this.drawTo(vec.doubleValue(0), vec.doubleValue(1)); + } + this.close(); + } + + /** + * Draw a line given a series of coordinates. + * + * Helper function that will use "move" for the first point, "lineto" for the + * remaining. + * + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath drawTo(double x, double y) { + return !isStarted() ? moveTo(x, y) : lineTo(x, y); + } + + /** + * Draw a line given a series of coordinates. + * + * Helper function that will use "move" for the first point, "lineto" for the + * remaining. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath drawTo(double[] xy) { + return !isStarted() ? moveTo(xy[0], xy[0]) : lineTo(xy[0], xy[1]); + } + + /** + * Draw a line given a series of coordinates. + * + * Helper function that will use "move" for the first point, "lineto" for the + * remaining. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath drawTo(Vector xy) { + return !isStarted() ? moveTo(xy.doubleValue(0), xy.doubleValue(1)) : lineTo(xy.doubleValue(0), xy.doubleValue(1)); + } + + /** + * Test whether the path drawing has already started. + * + * @return Path freshness + */ + public boolean isStarted() { + return lastaction != null; + } + + /** + * Draw a line to the given coordinates. + * + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath lineTo(double x, double y) { + if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY // + && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) { + append(SVGConstants.PATH_LINE_TO, x, y); + } + return this; + } + + /** + * Draw a line to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath lineTo(double[] xy) { + return lineTo(xy[0], xy[1]); + } + + /** + * Draw a line to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath lineTo(Vector xy) { + return lineTo(xy.doubleValue(0), xy.doubleValue(1)); + } + + /** + * Draw a line to the given relative coordinates. + * + * @param x relative coordinates + * @param y relative coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeLineTo(double x, double y) { + if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY // + && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) { + append(PATH_LINE_TO_RELATIVE, x, y); + } + return this; + } + + /** + * Draw a line to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeLineTo(double[] xy) { + return relativeLineTo(xy[0], xy[1]); + } + + /** + * Draw a line to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeLineTo(Vector xy) { + return relativeLineTo(xy.doubleValue(0), xy.doubleValue(1)); + } + + /** + * Draw a horizontal line to the given x coordinate. + * + * @param x new coordinates + * @return path object, for compact syntax. + */ + public SVGPath horizontalLineTo(double x) { + if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY) { + append(SVGConstants.PATH_HORIZONTAL_LINE_TO, x); + } + return this; + } + + /** + * Draw a horizontal line to the given relative x coordinate. + * + * @param x new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeHorizontalLineTo(double x) { + if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY) { + append(PATH_HORIZONTAL_LINE_TO_RELATIVE, x); + } + return this; + } + + /** + * Draw a vertical line to the given y coordinate. + * + * @param y new coordinate + * @return path object, for compact syntax. + */ + public SVGPath verticalLineTo(double y) { + if(y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) { + append(SVGConstants.PATH_VERTICAL_LINE_TO, y); + } + return this; + } + + /** + * Draw a vertical line to the given relative y coordinate. + * + * @param y new coordinate + * @return path object, for compact syntax. + */ + public SVGPath relativeVerticalLineTo(double y) { + if(y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) { + append(PATH_VERTICAL_LINE_TO_RELATIVE, y); + } + return this; + } + + /** + * Move to the given coordinates. + * + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath moveTo(double x, double y) { + if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY // + && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) { + append(SVGConstants.PATH_MOVE, x, y); + } + return this; + } + + /** + * Move to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath moveTo(double[] xy) { + return moveTo(xy[0], xy[1]); + } + + /** + * Move to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath moveTo(Vector xy) { + return moveTo(xy.doubleValue(0), xy.doubleValue(1)); + } + + /** + * Move to the given relative coordinates. + * + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeMoveTo(double x, double y) { + if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY // + && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) { + append(PATH_MOVE_RELATIVE, x, y); + } + return this; + } + + /** + * Move to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeMoveTo(double[] xy) { + return relativeMoveTo(xy[0], xy[1]); + } + + /** + * Move to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeMoveTo(Vector xy) { + return relativeMoveTo(xy.doubleValue(0), xy.doubleValue(1)); + } + + /** + * Cubic Bezier line to the given coordinates. + * + * @param c1x first control point x + * @param c1y first control point y + * @param c2x second control point x + * @param c2y second control point y + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath cubicTo(double c1x, double c1y, double c2x, double c2y, double x, double y) { + append(SVGConstants.PATH_CUBIC_TO, c1x, c1y, c2x, c2y, x, y); + return this; + } + + /** + * Cubic Bezier line to the given coordinates. + * + * @param c1xy first control point + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath cubicTo(double[] c1xy, double[] c2xy, double[] xy) { + append(SVGConstants.PATH_CUBIC_TO, c1xy[0], c1xy[1], c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Cubic Bezier line to the given coordinates. + * + * @param c1xy first control point + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath cubicTo(Vector c1xy, Vector c2xy, Vector xy) { + append(SVGConstants.PATH_CUBIC_TO, c1xy.doubleValue(0), c1xy.doubleValue(1), c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Cubic Bezier line to the given relative coordinates. + * + * @param c1x first control point x + * @param c1y first control point y + * @param c2x second control point x + * @param c2y second control point y + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeCubicTo(double c1x, double c1y, double c2x, double c2y, double x, double y) { + append(PATH_CUBIC_TO_RELATIVE, c1x, c1y, c2x, c2y, x, y); + return this; + } + + /** + * Cubic Bezier line to the given relative coordinates. + * + * @param c1xy first control point + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeCubicTo(double[] c1xy, double[] c2xy, double[] xy) { + append(PATH_CUBIC_TO_RELATIVE, c1xy[0], c1xy[1], c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Cubic Bezier line to the given relative coordinates. + * + * @param c1xy first control point + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeCubicTo(Vector c1xy, Vector c2xy, Vector xy) { + append(PATH_CUBIC_TO_RELATIVE, c1xy.doubleValue(0), c1xy.doubleValue(1), c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Smooth Cubic Bezier line to the given coordinates. + * + * @param c2x second control point x + * @param c2y second control point y + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath smoothCubicTo(double c2x, double c2y, double x, double y) { + append(PATH_SMOOTH_CUBIC_TO, c2x, c2y, x, y); + return this; + } + + /** + * Smooth Cubic Bezier line to the given coordinates. + * + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath smoothCubicTo(double[] c2xy, double[] xy) { + append(PATH_SMOOTH_CUBIC_TO, c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Smooth Cubic Bezier line to the given coordinates. + * + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath smoothCubicTo(Vector c2xy, Vector xy) { + append(PATH_SMOOTH_CUBIC_TO, c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Smooth Cubic Bezier line to the given relative coordinates. + * + * @param c2x second control point x + * @param c2y second control point y + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeSmoothCubicTo(double c2x, double c2y, double x, double y) { + append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2x, c2y, x, y); + return this; + } + + /** + * Smooth Cubic Bezier line to the given relative coordinates. + * + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeSmoothCubicTo(double[] c2xy, double[] xy) { + append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2xy[0], c2xy[1], xy[0], xy[1]); + return this; + } + + /** + * Smooth Cubic Bezier line to the given relative coordinates. + * + * @param c2xy second control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeSmoothCubicTo(Vector c2xy, Vector xy) { + append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Quadratic Bezier line to the given coordinates. + * + * @param c1x first control point x + * @param c1y first control point y + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath quadTo(double c1x, double c1y, double x, double y) { + append(SVGConstants.PATH_QUAD_TO, c1x, c1y, x, y); + return this; + } + + /** + * Quadratic Bezier line to the given coordinates. + * + * @param c1xy first control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath quadTo(double[] c1xy, double[] xy) { + append(SVGConstants.PATH_QUAD_TO, c1xy[0], c1xy[1], xy[0], xy[1]); + return this; + } + + /** + * Quadratic Bezier line to the given coordinates. + * + * @param c1xy first control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath quadTo(Vector c1xy, Vector xy) { + append(SVGConstants.PATH_QUAD_TO, c1xy.doubleValue(0), c1xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Quadratic Bezier line to the given relative coordinates. + * + * @param c1x first control point x + * @param c1y first control point y + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeQuadTo(double c1x, double c1y, double x, double y) { + append(PATH_QUAD_TO_RELATIVE, c1x, c1y, x, y); + return this; + } + + /** + * Quadratic Bezier line to the given relative coordinates. + * + * @param c1xy first control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeQuadTo(double[] c1xy, double[] xy) { + append(PATH_QUAD_TO_RELATIVE, c1xy[0], c1xy[1], xy[0], xy[1]); + return this; + } + + /** + * Quadratic Bezier line to the given relative coordinates. + * + * @param c1xy first control point + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeQuadTo(Vector c1xy, Vector xy) { + append(PATH_QUAD_TO_RELATIVE, c1xy.doubleValue(0), c1xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Smooth quadratic Bezier line to the given coordinates. + * + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath smoothQuadTo(double x, double y) { + append(SVGConstants.PATH_SMOOTH_QUAD_TO, x, y); + return this; + } + + /** + * Smooth quadratic Bezier line to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath smoothQuadTo(double[] xy) { + append(SVGConstants.PATH_SMOOTH_QUAD_TO, xy[0], xy[1]); + return this; + } + + /** + * Smooth quadratic Bezier line to the given coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath smoothQuadTo(Vector xy) { + append(SVGConstants.PATH_SMOOTH_QUAD_TO, xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Smooth quadratic Bezier line to the given relative coordinates. + * + * @param x new coordinates + * @param y new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeSmoothQuadTo(double x, double y) { + append(PATH_SMOOTH_QUAD_TO_RELATIVE, x, y); + return this; + } + + /** + * Smooth quadratic Bezier line to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeSmoothQuadTo(double[] xy) { + append(PATH_SMOOTH_QUAD_TO_RELATIVE, xy[0], xy[1]); + return this; + } + + /** + * Smooth quadratic Bezier line to the given relative coordinates. + * + * @param xy new coordinates + * @return path object, for compact syntax. + */ + public SVGPath relativeSmoothQuadTo(Vector xy) { + append(PATH_SMOOTH_QUAD_TO_RELATIVE, xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Elliptical arc curve to the given coordinates. + * + * @param rx x radius + * @param ry y radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param x new coordinates + * @param y new coordinates + */ + public SVGPath ellipticalArc(double rx, double ry, double ar, double la, double sp, double x, double y) { + append(SVGConstants.PATH_ARC, rx, ry, ar, la, sp, x, y); + return this; + } + + /** + * Elliptical arc curve to the given coordinates. + * + * @param rx x radius + * @param ry y radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param xy new coordinates + */ + public SVGPath ellipticalArc(double rx, double ry, double ar, double la, double sp, double[] xy) { + append(SVGConstants.PATH_ARC, rx, ry, ar, la, sp, xy[0], xy[1]); + return this; + } + + /** + * Elliptical arc curve to the given coordinates. + * + * @param rxy radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param xy new coordinates + */ + public SVGPath ellipticalArc(Vector rxy, double ar, double la, double sp, Vector xy) { + append(SVGConstants.PATH_ARC, rxy.doubleValue(0), rxy.doubleValue(1), ar, la, sp, xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Elliptical arc curve to the given relative coordinates. + * + * @param rx x radius + * @param ry y radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param x new coordinates + * @param y new coordinates + */ + public SVGPath relativeEllipticalArc(double rx, double ry, double ar, double la, double sp, double x, double y) { + append(PATH_ARC_RELATIVE, rx, ry, ar, la, sp, x, y); + return this; + } + + /** + * Elliptical arc curve to the given relative coordinates. + * + * @param rx x radius + * @param ry y radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param xy new coordinates + */ + public SVGPath relativeEllipticalArc(double rx, double ry, double ar, double la, double sp, double[] xy) { + append(PATH_ARC_RELATIVE, rx, ry, ar, la, sp, xy[0], xy[1]); + return this; + } + + /** + * Elliptical arc curve to the given relative coordinates. + * + * @param rxy radius + * @param ar x-axis-rotation + * @param la large arc flag, if angle >= 180 deg + * @param sp sweep flag, if arc will be drawn in positive-angle direction + * @param xy new coordinates + */ + public SVGPath relativeEllipticalArc(Vector rxy, double ar, double la, double sp, Vector xy) { + append(PATH_ARC_RELATIVE, rxy.doubleValue(0), rxy.doubleValue(1), ar, la, sp, xy.doubleValue(0), xy.doubleValue(1)); + return this; + } + + /** + * Append an action to the current path. + * + * @param action Current action + * @param ds coordinates. + */ + private void append(String action, double... ds) { + if(lastaction != action) { + buf.append(action); + lastaction = action; + } + for(double d : ds) { + buf.append(SVGUtil.FMT.format(d)); + buf.append(' '); + } + } + + /** + * Close the path. + * + * @return path object, for compact syntax. + */ + public SVGPath close() { + if(lastaction != SVGConstants.PATH_CLOSE) { + buf.append(SVGConstants.PATH_CLOSE); + lastaction = SVGConstants.PATH_CLOSE; + } + return this; + } + + /** + * Turn the path buffer into an SVG element. + * + * @param document Document context (= element factory) + * @return SVG Element + */ + public Element makeElement(Document document) { + Element elem = SVGUtil.svgElement(document, SVGConstants.SVG_PATH_TAG); + elem.setAttribute(SVGConstants.SVG_D_ATTRIBUTE, buf.toString()); + return elem; + } + + /** + * Turn the path buffer into an SVG element. + * + * @param plot Plot context (= element factory) + * @return SVG Element + */ + public Element makeElement(SVGPlot plot) { + Element elem = plot.svgElement(SVGConstants.SVG_PATH_TAG); + elem.setAttribute(SVGConstants.SVG_D_ATTRIBUTE, buf.toString()); + return elem; + } + + /** + * Return the SVG serialization of the path. + */ + @Override + public String toString() { + return buf.toString(); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java new file mode 100755 index 00000000..7eb51819 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java @@ -0,0 +1,720 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.batik.transcoder.Transcoder; +import org.apache.batik.transcoder.TranscoderException; +import org.apache.batik.transcoder.TranscoderInput; +import org.apache.batik.transcoder.TranscoderOutput; +import org.apache.batik.transcoder.XMLAbstractTranscoder; +import org.apache.batik.transcoder.image.JPEGTranscoder; +import org.apache.batik.transcoder.image.PNGTranscoder; +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.DOMImplementation; +import org.w3c.dom.Document; +import org.w3c.dom.DocumentType; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGDocument; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.utilities.FileUtil; +import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; +import de.lmu.ifi.dbs.elki.visualization.batikutil.CloneInlineImages; +import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailTranscoder; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; + +/** + * Base class for SVG plots. Provides some basic functionality such as element + * creation, axis plotting, markers and number formatting for SVG. + * + * @author Erich Schubert + * + * @apiviz.landmark + * @apiviz.composedOf CSSClassManager + * @apiviz.composedOf UpdateRunner + * @apiviz.composedOf SVGDocument + * @apiviz.has Element oneway - - contains + * @apiviz.has UpdateSynchronizer oneway - - synchronizesWith + */ +public class SVGPlot { + /** + * Default JPEG quality setting + */ + public static final double DEFAULT_QUALITY = 0.85; + + /** + * Attribute to block export of element. + */ + public static final String NO_EXPORT_ATTRIBUTE = "noexport"; + + /** + * Batik DOM implementation. + */ + private static final DOMImplementation BATIK_DOM; + + /** + * DOM implementations to try. + */ + private static final String[] BATIK_DOMS = { // + "org.apache.batik.anim.dom.SVGDOMImplementation", // Batik 1.8 + "org.apache.batik.dom.svg.SVGDOMImplementation", // Batik 1.7 + "com.sun.org.apache.xerces.internal.dom.DOMImplementationImpl", // Untested + }; + + // Locate a usable DOM implementation. + static { + DOMImplementation dom = null; + for(String s : BATIK_DOMS) { + try { + Class<?> c = Class.forName(s); + Method m = c.getDeclaredMethod("getDOMImplementation"); + DOMImplementation ret = DOMImplementation.class.cast(m.invoke(null)); + if(ret != null) { + dom = ret; + break; + } + } + catch(Exception e) { + continue; + } + } + BATIK_DOM = dom; + } + + /** + * SVG document we plot to. + */ + private SVGDocument document; + + /** + * Root element of the document. + */ + private Element root; + + /** + * Definitions element of the document. + */ + private Element defs; + + /** + * Primary style information + */ + private Element style; + + /** + * CSS class manager + */ + private CSSClassManager cssman; + + /** + * Manage objects with an id. + */ + private HashMap<String, WeakReference<Element>> objWithId = new HashMap<>(); + + /** + * Registers changes of this SVGPlot. + */ + private UpdateRunner runner = new UpdateRunner(this); + + /** + * Flag whether Batik interactions should be disabled. + */ + private boolean disableInteractions = false; + + /** + * Create a new plotting document. + */ + public SVGPlot() { + super(); + // Get a DOMImplementation. + DOMImplementation domImpl = getDomImpl(); + DocumentType dt = domImpl.createDocumentType(SVGConstants.SVG_SVG_TAG, SVGConstants.SVG_PUBLIC_ID, SVGConstants.SVG_SYSTEM_ID); + // Workaround: sometimes DocumentType doesn't work right, which + // causes problems with + // serialization... + if(dt.getName() == null) { + dt = null; + } + + document = (SVGDocument) domImpl.createDocument(SVGConstants.SVG_NAMESPACE_URI, SVGConstants.SVG_SVG_TAG, dt); + + root = document.getDocumentElement(); + // setup common SVG namespaces + root.setAttribute(SVGConstants.XMLNS_PREFIX, SVGConstants.SVG_NAMESPACE_URI); + root.setAttributeNS(SVGConstants.XMLNS_NAMESPACE_URI, SVGConstants.XMLNS_PREFIX + ":" + SVGConstants.XLINK_PREFIX, SVGConstants.XLINK_NAMESPACE_URI); + + // create element for SVG definitions + defs = svgElement(SVGConstants.SVG_DEFS_TAG); + root.appendChild(defs); + + // create element for Stylesheet information. + style = SVGUtil.makeStyleElement(document); + root.appendChild(style); + + // create a CSS class manager. + cssman = new CSSClassManager(); + } + + /** + * Get a suitable SVG DOM implementation from Batik 1.7 or 1.8. + * + * @return DOM implementation + */ + public static DOMImplementation getDomImpl() { + if(BATIK_DOM == null) { + throw new AbortException("No usable Apache Batik SVG DOM could be located."); + } + return BATIK_DOM; + } + + /** + * Clean up the plot. + */ + public void dispose() { + runner.clear(); + } + + /** + * Create a SVG element in the SVG namespace. Non-static version. + * + * @param name node name + * @return new SVG element. + */ + public Element svgElement(String name) { + return SVGUtil.svgElement(document, name); + } + + /** + * Create a SVG rectangle + * + * @param x X coordinate + * @param y Y coordinate + * @param w Width + * @param h Height + * @return new element + */ + public Element svgRect(double x, double y, double w, double h) { + return SVGUtil.svgRect(document, x, y, w, h); + } + + /** + * Create a SVG circle + * + * @param cx center X + * @param cy center Y + * @param r radius + * @return new element + */ + public Element svgCircle(double cx, double cy, double r) { + return SVGUtil.svgCircle(document, cx, cy, r); + } + + /** + * Create a SVG line element + * + * @param x1 first point x + * @param y1 first point y + * @param x2 second point x + * @param y2 second point y + * @return new element + */ + public Element svgLine(double x1, double y1, double x2, double y2) { + return SVGUtil.svgLine(document, x1, y1, x2, y2); + } + + /** + * Create a SVG text element. + * + * @param x first point x + * @param y first point y + * @param text Content of text element. + * @return New text element. + */ + public Element svgText(double x, double y, String text) { + return SVGUtil.svgText(document, x, y, text); + } + + /** + * Convert screen coordinates to element coordinates. + * + * @param tag Element to convert the coordinates for + * @param evt Event object + * @return Coordinates + */ + public SVGPoint elementCoordinatesFromEvent(Element tag, Event evt) { + return SVGUtil.elementCoordinatesFromEvent(document, tag, evt); + } + + /** + * Retrieve the SVG document. + * + * @return resulting document. + */ + public SVGDocument getDocument() { + return document; + } + + /** + * Getter for root element. + * + * @return DOM element + */ + public Element getRoot() { + return root; + } + + /** + * Getter for definitions section + * + * @return DOM element + */ + public Element getDefs() { + return defs; + } + + /** + * Getter for style element. + * + * @return stylesheet DOM element + * @deprecated Contents will be overwritten by CSS class manager! + */ + @Deprecated + public Element getStyle() { + return style; + } + + /** + * Get the plots CSS class manager. + * + * Note that you need to invoke {@link #updateStyleElement()} to make changes + * take effect. + * + * @return CSS class manager. + */ + public CSSClassManager getCSSClassManager() { + return cssman; + } + + /** + * Convenience method to add a CSS class or log an error. + * + * @param cls CSS class to add. + */ + public void addCSSClassOrLogError(CSSClass cls) { + try { + cssman.addClass(cls); + } + catch(CSSNamingConflict e) { + de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e); + } + } + + /** + * Update style element - invoke this appropriately after any change to the + * CSS styles. + */ + public void updateStyleElement() { + // TODO: this should be sufficient - why does Batik occasionally not pick up + // the changes unless we actually replace the style element itself? + // cssman.updateStyleElement(document, style); + Element newstyle = cssman.makeStyleElement(document); + style.getParentNode().replaceChild(newstyle, style); + style = newstyle; + } + + /** + * Save document into a SVG file. + * + * References PNG images from the temporary files will be inlined + * automatically. + * + * @param file Output filename + * @throws IOException On write errors + * @throws TransformerFactoryConfigurationError Transformation error + * @throws TransformerException Transformation error + */ + public void saveAsSVG(File file) throws IOException, TransformerFactoryConfigurationError, TransformerException { + OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); + // TODO embed linked images. + javax.xml.transform.Result result = new StreamResult(out); + SVGDocument doc = cloneDocument(); + // Use a transformer for pretty printing + Transformer xformer = TransformerFactory.newInstance().newTransformer(); + xformer.setOutputProperty(OutputKeys.INDENT, "yes"); + xformer.transform(new DOMSource(doc), result); + out.flush(); + out.close(); + } + + /** + * Transcode a document into a file using the given transcoder. + * + * @param file Output file + * @param transcoder Transcoder to use + * @throws IOException On write errors + * @throws TranscoderException On input/parsing errors + */ + protected void transcode(File file, Transcoder transcoder) throws IOException, TranscoderException { + // Disable validation, performance is more important here (thumbnails!) + transcoder.addTranscodingHint(XMLAbstractTranscoder.KEY_XML_PARSER_VALIDATING, Boolean.FALSE); + SVGDocument doc = cloneDocument(); + TranscoderInput input = new TranscoderInput(doc); + OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); + TranscoderOutput output = new TranscoderOutput(out); + transcoder.transcode(input, output); + out.flush(); + out.close(); + } + + /** + * Clone the SVGPlot document for transcoding. + * + * This will usually be necessary for exporting the SVG document if it is + * currently being displayed: otherwise, we break the Batik rendering trees. + * (Discovered by Simon). + * + * @return cloned document + */ + protected SVGDocument cloneDocument() { + return (SVGDocument) new CloneNoExport().cloneDocument(getDomImpl(), document); + } + + /** + * Transcode file to PDF. + * + * @param file Output filename + * @throws IOException On write errors + * @throws TranscoderException On input/parsing errors. + * @throws ClassNotFoundException PDF transcoder not installed + */ + public void saveAsPDF(File file) throws IOException, TranscoderException, ClassNotFoundException { + try { + Object t = Class.forName("org.apache.fop.svg.PDFTranscoder").newInstance(); + transcode(file, (Transcoder) t); + } + catch(InstantiationException e) { + throw new ClassNotFoundException("Could not instantiate PDF transcoder - is Apache FOP installed?", e); + } + catch(IllegalAccessException e) { + throw new ClassNotFoundException("Could not instantiate PDF transcoder - is Apache FOP installed?", e); + } + } + + /** + * Transcode file to PS. + * + * @param file Output filename + * @throws IOException On write errors + * @throws TranscoderException On input/parsing errors. + * @throws ClassNotFoundException PS transcoder not installed + */ + public void saveAsPS(File file) throws IOException, TranscoderException, ClassNotFoundException { + try { + Object t = Class.forName("org.apache.fop.render.ps.PSTranscoder").newInstance(); + transcode(file, (Transcoder) t); + } + catch(InstantiationException e) { + throw new ClassNotFoundException("Could not instantiate PS transcoder - is Apache FOP installed?", e); + } + catch(IllegalAccessException e) { + throw new ClassNotFoundException("Could not instantiate PS transcoder - is Apache FOP installed?", e); + } + } + + /** + * Transcode file to EPS. + * + * @param file Output filename + * @throws IOException On write errors + * @throws TranscoderException On input/parsing errors. + * @throws ClassNotFoundException EPS transcoder not installed + */ + public void saveAsEPS(File file) throws IOException, TranscoderException, ClassNotFoundException { + try { + Object t = Class.forName("org.apache.fop.render.ps.EPSTranscoder").newInstance(); + transcode(file, (Transcoder) t); + } + catch(InstantiationException e) { + throw new ClassNotFoundException("Could not instantiate EPS transcoder - is Apache FOP installed?", e); + } + catch(IllegalAccessException e) { + throw new ClassNotFoundException("Could not instantiate EPS transcoder - is Apache FOP installed?", e); + } + } + + /** + * Test whether FOP were installed (for PDF, PS and EPS output support). + * + * @return true when FOP is available. + */ + public static boolean hasFOPInstalled() { + try { + Class<?> c1 = Class.forName("org.apache.fop.svg.PDFTranscoder"); + Class<?> c2 = Class.forName("org.apache.fop.render.ps.PSTranscoder"); + Class<?> c3 = Class.forName("org.apache.fop.render.ps.EPSTranscoder"); + return (c1 != null) && (c2 != null) && (c3 != null); + } + catch(ClassNotFoundException e) { + return false; + } + } + + /** + * Transcode file to PNG. + * + * @param file Output filename + * @param width Width + * @param height Height + * @throws IOException On write errors + * @throws TranscoderException On input/parsing errors. + */ + public void saveAsPNG(File file, int width, int height) throws IOException, TranscoderException { + PNGTranscoder t = new PNGTranscoder(); + t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width)); + t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height)); + transcode(file, t); + } + + /** + * Transcode file to JPEG. + * + * @param file Output filename + * @param width Width + * @param height Height + * @param quality JPEG quality setting, between 0.0 and 1.0 + * @throws IOException On write errors + * @throws TranscoderException On input/parsing errors. + */ + public void saveAsJPEG(File file, int width, int height, double quality) throws IOException, TranscoderException { + JPEGTranscoder t = new JPEGTranscoder(); + t.addTranscodingHint(JPEGTranscoder.KEY_WIDTH, new Float(width)); + t.addTranscodingHint(JPEGTranscoder.KEY_HEIGHT, new Float(height)); + t.addTranscodingHint(JPEGTranscoder.KEY_QUALITY, new Float(quality)); + transcode(file, t); + } + + /** + * Transcode file to JPEG. + * + * @param file Output filename + * @param width Width + * @param height Height + * @throws IOException On write errors + * @throws TranscoderException On input/parsing errors. + */ + public void saveAsJPEG(File file, int width, int height) throws IOException, TranscoderException { + saveAsJPEG(file, width, height, DEFAULT_QUALITY); + } + + /** + * Save a file trying to auto-guess the file type. + * + * @param file File name + * @param width Width (for pixel formats) + * @param height Height (for pixel formats) + * @param quality Quality (for lossy compression) + * @throws IOException on file write errors or unrecognized file extensions + * @throws TranscoderException on transcoding errors + * @throws TransformerFactoryConfigurationError on transcoding errors + * @throws TransformerException on transcoding errors + * @throws ClassNotFoundException when the transcoder was not installed + */ + public void saveAsANY(File file, int width, int height, double quality) throws IOException, TranscoderException, TransformerFactoryConfigurationError, TransformerException, ClassNotFoundException { + String extension = FileUtil.getFilenameExtension(file); + if(extension.equals("svg")) { + saveAsSVG(file); + } + else if(extension.equals("pdf")) { + saveAsPDF(file); + } + else if(extension.equals("ps")) { + saveAsPS(file); + } + else if(extension.equals("eps")) { + saveAsEPS(file); + } + else if(extension.equals("png")) { + saveAsPNG(file, width, height); + } + else if(extension.equals("jpg") || extension.equals("jpeg")) { + saveAsJPEG(file, width, height, quality); + } + else { + throw new IOException("Unknown file extension: " + extension); + } + } + + /** + * Convert the SVG to a thumbnail image. + * + * @param width Width of thumbnail + * @param height Height of thumbnail + * @return Buffered image + */ + public BufferedImage makeAWTImage(int width, int height) throws TranscoderException { + ThumbnailTranscoder t = new ThumbnailTranscoder(); + t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width)); + t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height)); + // Don't clone. Assume this is used safely. + TranscoderInput input = new TranscoderInput(document); + t.transcode(input, null); + return t.getLastImage(); + } + + /** + * Dump the SVG plot to a debug file. + */ + public void dumpDebugFile() { + try { + File f = File.createTempFile("elki-debug", ".svg"); + f.deleteOnExit(); + this.saveAsSVG(f); + LoggingUtil.warning("Saved debug file to: " + f.getAbsolutePath()); + } + catch(Throwable err) { + // Ignore. + } + } + + /** + * Add an object id. + * + * @param id ID + * @param obj Element + */ + public void putIdElement(String id, Element obj) { + objWithId.put(id, new WeakReference<>(obj)); + } + + /** + * Get an element by its id. + * + * @param id ID + * @return Element + */ + public Element getIdElement(String id) { + WeakReference<Element> ref = objWithId.get(id); + return (ref != null) ? ref.get() : null; + } + + /** + * Get all used DOM Ids in this plot. + * + * @return Collection of DOM element IDs. + */ + protected Collection<String> getAllIds() { + return objWithId.keySet(); + } + + /** + * Schedule an update. + * + * @param runnable Runnable to schedule + */ + public void scheduleUpdate(Runnable runnable) { + runner.invokeLater(runnable); + } + + /** + * Assign an update synchronizer. + * + * @param sync Update synchronizer + */ + public void synchronizeWith(UpdateSynchronizer sync) { + runner.synchronizeWith(sync); + } + + /** + * Detach from synchronization. + * + * @param sync Update synchronizer to detach from. + */ + public void unsynchronizeWith(UpdateSynchronizer sync) { + runner.unsynchronizeWith(sync); + } + + /** + * Get Batik disable default interactions flag. + * + * @return true when Batik default interactions are disabled + */ + public boolean getDisableInteractions() { + return disableInteractions; + } + + /** + * Disable Batik predefined interactions. + * + * @param disable Flag + */ + public void setDisableInteractions(boolean disable) { + disableInteractions = disable; + } + + /** + * Class to skip nodes during cloning that have the "noexport" attribute set. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + protected class CloneNoExport extends CloneInlineImages { + @Override + public Node cloneNode(Document doc, Node eold) { + // Skip elements with noexport attribute set + if(eold instanceof Element) { + Element eeold = (Element) eold; + String vis = eeold.getAttribute(NO_EXPORT_ATTRIBUTE); + if(vis != null && vis.length() > 0) { + return null; + } + } + return super.cloneNode(doc, eold); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java new file mode 100644 index 00000000..dd16070d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java @@ -0,0 +1,158 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.text.NumberFormat; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +/** + * Draw a score bar. Essentially like a progress bar, left-to-right, displaying + * a relative score. + * + * @author Sascha Goldhofer + */ +// TODO: refactor to get a progress bar? +public class SVGScoreBar { + /** + * Value, minimum and maximum values + */ + protected double val, min = 0., max = 1.; + + /** + * Reversed flag. + */ + protected boolean reversed = false; + + /** + * Label (on the right) + */ + protected String label = null; + + /** + * Number format, set to print the actual score + */ + private NumberFormat format = null; + + /** + * Constructor. + */ + public SVGScoreBar() { + // Nothing to do here. + } + + /** + * Set the fill of the score bar. + * + * @param val Value + * @param min Minimum value + * @param max Maximum value + */ + public void setFill(double val, double min, double max) { + this.val = val; + this.min = min; + this.max = max; + } + + /** + * Set the reversed flag. + * + * @param reversed Reversed flag. + */ + public void setReversed(boolean reversed) { + this.reversed = reversed; + } + + /** + * Set label (right of the bar) + * + * @param text Label text + */ + public void addLabel(String text) { + this.label = text; + } + + /** + * To show score values, set a number format + * + * @param format Number format + */ + public void showValues(NumberFormat format) { + this.format = format; + } + + /** + * Build the actual element + * + * @param svgp Plot to draw to + * @param x X coordinate + * @param y Y coordinate + * @param width Width + * @param height Height + * @return new element + */ + public Element build(SVGPlot svgp, double x, double y, double width, double height) { + Element barchart = svgp.svgElement(SVGConstants.SVG_G_TAG); + + // TODO: use style library for colors! + Element bar = svgp.svgRect(x, y, width, height); + bar.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#a0a0a0"); + bar.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0"); + bar.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, String.valueOf(height * 0.01)); + barchart.appendChild(bar); + + if(val >= min && val <= max && min < max) { + final double frame = 0.02 * height; + double fpos = (val - min) / (max - min) * (width - 2 * frame); + Element chart; + if(reversed) { + chart = svgp.svgRect(x + frame + fpos, y + frame, width - fpos - 2 * frame, height - 2 * frame); + } + else { + chart = svgp.svgRect(x + frame, y + frame, fpos, height - 2 * frame); + } + chart.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1"); + chart.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0"); + chart.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, String.valueOf(height * 0.01)); + barchart.appendChild(chart); + } + + // Draw the values: + if(format != null) { + String num = Double.isNaN(val) ? "NaN" : format.format(val); + Element lbl = svgp.svgText(x + 0.05 * width, y + 0.75 * height, num); + lbl.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + 0.75 * height + "; font-weight: bold"); + barchart.appendChild(lbl); + } + + // Draw the label + if(label != null) { + Element lbl = svgp.svgText(x + 1.05 * width, y + 0.75 * height, label); + lbl.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + 0.75 * height + "; font-weight: normal"); + barchart.appendChild(lbl); + } + return barchart; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java new file mode 100755 index 00000000..5559e602 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java @@ -0,0 +1,263 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; + +/** + * Class to draw a simple axis with tick marks on the plot. + * + * @author Erich Schubert + * + * @apiviz.uses CSSClass + * @apiviz.uses CSSClassManager + * @apiviz.uses LinearScale + * @apiviz.uses StyleLibrary + * @apiviz.uses Element oneway - - «create» + */ +public class SVGSimpleLinearAxis { + /** + * Flag for axis label position. First char: right-hand or left-hand side of + * line. Second char: text alignment + * + * @apiviz.exclude + */ + private enum Alignment { + LL, RL, LC, RC, LR, RR + } + + /** + * Labeling style: left-handed, right-handed, no ticks, labels at ends + * + * @apiviz.exclude + */ + public enum LabelStyle { + LEFTHAND, RIGHTHAND, NOLABELS, NOTHING, ENDLABEL + } + + /** + * CSS class name for the axes + */ + private static final String CSS_AXIS = "axis"; + + /** + * CSS class name for the axes + */ + private static final String CSS_AXIS_TICK = "axis-tick"; + + /** + * CSS class name for the axes + */ + private static final String CSS_AXIS_LABEL = "axis-label"; + + /** + * Register CSS classes with a {@link CSSClassManager} + * + * @param owner Owner of the CSS classes + * @param manager Manager to register the classes with + * @throws CSSNamingConflict when a name clash occurs + */ + private static void setupCSSClasses(Object owner, CSSClassManager manager, StyleLibrary style) throws CSSNamingConflict { + if(!manager.contains(CSS_AXIS)) { + CSSClass axis = new CSSClass(owner, CSS_AXIS); + axis.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.AXIS)); + axis.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.AXIS)); + manager.addClass(axis); + } + if(!manager.contains(CSS_AXIS_TICK)) { + CSSClass tick = new CSSClass(owner, CSS_AXIS_TICK); + tick.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.AXIS_TICK)); + tick.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.AXIS_TICK)); + manager.addClass(tick); + } + if(!manager.contains(CSS_AXIS_LABEL)) { + CSSClass label = new CSSClass(owner, CSS_AXIS_LABEL); + label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL)); + label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL)); + label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL)); + manager.addClass(label); + } + } + + /** + * Plot an axis with appropriate scales + * + * @param plot Plot object + * @param parent Containing element + * @param scale axis scale information + * @param x1 starting coordinate + * @param y1 starting coordinate + * @param x2 ending coordinate + * @param y2 ending coordinate + * @param labelstyle Style for placing the labels + * @param style Style library + * @throws CSSNamingConflict when a conflict occurs in CSS + */ + public static void drawAxis(SVGPlot plot, Element parent, LinearScale scale, double x1, double y1, double x2, double y2, LabelStyle labelstyle, StyleLibrary style) throws CSSNamingConflict { + assert (parent != null); + Element line = plot.svgLine(x1, y1, x2, y2); + SVGUtil.setCSSClass(line, CSS_AXIS); + parent.appendChild(line); + + final double tx = x2 - x1; + final double ty = y2 - y1; + // ticks are orthogonal + final double tw = ty * 0.01; + final double th = -tx * 0.01; + + // choose where to print labels. + final boolean labels, ticks; + switch(labelstyle){ + case LEFTHAND: + case RIGHTHAND: + labels = true; + ticks = true; + break; + case NOLABELS: + labels = false; + ticks = true; + break; + case ENDLABEL: // end labels are handle specially + case NOTHING: + default: + labels = false; + ticks = false; + } + Alignment pos = Alignment.LL; + if(labels) { + double angle = Math.atan2(ty, tx); + // System.err.println(tx + " " + (-ty) + " " + angle); + if(angle > 2.6) { // pi .. 2.6 = 180 .. 150 + pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RC : Alignment.LC; + } + else if(angle > 0.5) { // 2.3 .. 0.7 = 130 .. 40 + pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RR : Alignment.LL; + } + else if(angle > -0.5) { // 0.5 .. -0.5 = 30 .. -30 + pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RC : Alignment.LC; + } + else if(angle > -2.6) { // -0.5 .. -2.6 = -30 .. -150 + pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RL : Alignment.LR; + } + else { // -2.6 .. -pi = -150 .. -180 + pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RC : Alignment.LC; + } + } + // vertical text offset; align approximately with middle instead of + // baseline. + double textvoff = style.getTextSize(StyleLibrary.AXIS_LABEL) * .35; + + // draw ticks on x axis + if(ticks || labels) { + int sw = 1; + { // Compute how many ticks to draw + int numticks = (int) ((scale.getMax() - scale.getMin()) / scale.getRes()); + double tlen = Math.sqrt(tx * tx + ty * ty); + double minl = 10 * style.getLineWidth(StyleLibrary.AXIS_TICK); + // Try proper divisors first. + if(sw * tlen / numticks < minl) { + for(int i = 2; i <= (numticks >> 1); i++) { + if(numticks % i == 0) { + if(i * tlen / numticks >= minl) { + sw = i; + break; + } + } + } + } + // Otherwise, also allow non-divisors. + if(sw * tlen / numticks < minl) { + sw = (int)Math.floor(minl * numticks / tlen); + } + } + for(double tick = scale.getMin(); tick <= scale.getMax() + scale.getRes() / 10; tick += sw * scale.getRes()) { + double x = x1 + tx * scale.getScaled(tick); + double y = y1 + ty * scale.getScaled(tick); + if(ticks) { + // This is correct. Vectors: (vec - tvec) to (vec + tvec) + Element tickline = plot.svgLine(x - tw, y - th, x + tw, y + th); + SVGUtil.setAtt(tickline, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_TICK); + parent.appendChild(tickline); + } + // draw labels + if(labels) { + double tex = x; + double tey = y; + switch(pos){ + case LL: + case LC: + case LR: + tex = x + tw * 2.5; + tey = y + th * 2.5 + textvoff; + break; + case RL: + case RC: + case RR: + tex = x - tw * 2.5; + tey = y - th * 2.5 + textvoff; + } + Element text = plot.svgText(tex, tey, scale.formatValue(tick)); + text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL); + switch(pos){ + case LL: + case RL: + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_START_VALUE); + break; + case LC: + case RC: + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); + break; + case LR: + case RR: + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_END_VALUE); + break; + } + parent.appendChild(text); + } + } + } + if(labelstyle == LabelStyle.ENDLABEL) { + { + Element text = plot.svgText(x1 - tx * 0.02, y1 - ty * 0.02 + textvoff, scale.formatValue(scale.getMin())); + text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL); + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); + parent.appendChild(text); + } + { + Element text = plot.svgText(x2 + tx * 0.02, y2 + ty * 0.02 + textvoff, scale.formatValue(scale.getMax())); + text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL); + text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); + parent.appendChild(text); + } + } + setupCSSClasses(plot, plot.getCSSClassManager(), style); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java new file mode 100755 index 00000000..b85af07e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java @@ -0,0 +1,703 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.Color; +import java.text.NumberFormat; +import java.util.Locale; + +import javax.swing.text.html.StyleSheet; + +import org.apache.batik.dom.events.DOMMouseEvent; +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGDocument; +import org.w3c.dom.svg.SVGLocatable; +import org.w3c.dom.svg.SVGMatrix; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import gnu.trove.map.hash.TObjectIntHashMap; + +/** + * Utility class for SVG processing. + * + * Much of the classes are to allow easier attribute setting (conversion to + * string) and Namespace handling + * + * @author Erich Schubert + * + * @apiviz.uses Element oneway - - «create» + */ +public final class SVGUtil { + /** + * Formatter to output numbers in a valid SVG number format. + */ + public static final NumberFormat FMT = NumberFormat.getInstance(Locale.ROOT); + + static { + FMT.setMaximumFractionDigits(10); + FMT.setGroupingUsed(false); + } + + /** + * Hourglass object. + */ + final public static String HOURGLASS_PATH = "M.35 .2 L.65 .2 L.65 .3 L.35 .7 L.35 .8 L.65 .8 L.65 .7 L.35 .3 Z"; + + /** + * Hourglass style. + */ + final public static String HOURGLASS_STYLE = "stroke: black; stroke-width: .01; fill: grey; opacity: .2"; + + /** + * Throbber path. + */ + final public static String THROBBER_PATH = "M.5,.25 a.25,.25 0 0 1 .1766,.42635 l-.0589 -.0589 a-.1766 -.1766 0 0 0 -.1178,-.2835 z"; + + /** + * Throbber style. + */ + final public static String THROBBER_STYLE = "fill: #3d7fe6; opacity: .2"; + + /** + * SVG color names conversion. + */ + final private static TObjectIntHashMap<String> SVG_COLOR_NAMES; + + /** + * Key not found value. Not a reasonable color, fully transparent! + */ + final private static int NO_VALUE = 0x00123456; + + static { + // Build a reasonably sized hashmap. Use 0 + SVG_COLOR_NAMES = new TObjectIntHashMap<>(90, .8f, NO_VALUE); + // List taken from SVG specification: + // http://www.w3.org/TR/SVG/types.html#ColorKeywords + SVG_COLOR_NAMES.put("aliceblue", 0xFFF0F8FF); + SVG_COLOR_NAMES.put("antiquewhite", 0xFFFAEBD7); + SVG_COLOR_NAMES.put("aqua", 0xFF00FFFF); + SVG_COLOR_NAMES.put("aquamarine", 0xFF7FFFD4); + SVG_COLOR_NAMES.put("azure", 0xFFF0FFFF); + SVG_COLOR_NAMES.put("beige", 0xFFF5F5DC); + SVG_COLOR_NAMES.put("bisque", 0xFFFFE4C4); + SVG_COLOR_NAMES.put("black", 0xFF000000); + SVG_COLOR_NAMES.put("blanchedalmond", 0xFFFFEBCD); + SVG_COLOR_NAMES.put("blue", 0xFF0000FF); + SVG_COLOR_NAMES.put("blueviolet", 0xFF8A2BE2); + SVG_COLOR_NAMES.put("brown", 0xFFA52A2A); + SVG_COLOR_NAMES.put("burlywood", 0xFFDEB887); + SVG_COLOR_NAMES.put("cadetblue", 0xFF5F9EA0); + SVG_COLOR_NAMES.put("chartreuse", 0xFF7FFF00); + SVG_COLOR_NAMES.put("chocolate", 0xFFD2691E); + SVG_COLOR_NAMES.put("coral", 0xFFFF7F50); + SVG_COLOR_NAMES.put("cornflowerblue", 0xFF6495ED); + SVG_COLOR_NAMES.put("cornsilk", 0xFFFFF8DC); + SVG_COLOR_NAMES.put("crimson", 0xFFDC143C); + SVG_COLOR_NAMES.put("cyan", 0xFF00FFFF); + SVG_COLOR_NAMES.put("darkblue", 0xFF00008B); + SVG_COLOR_NAMES.put("darkcyan", 0xFF008B8B); + SVG_COLOR_NAMES.put("darkgoldenrod", 0xFFB8860B); + SVG_COLOR_NAMES.put("darkgray", 0xFFA9A9A9); + SVG_COLOR_NAMES.put("darkgreen", 0xFF006400); + SVG_COLOR_NAMES.put("darkgrey", 0xFFA9A9A9); + SVG_COLOR_NAMES.put("darkkhaki", 0xFFBDB76B); + SVG_COLOR_NAMES.put("darkmagenta", 0xFF8B008B); + SVG_COLOR_NAMES.put("darkolivegreen", 0xFF556B2F); + SVG_COLOR_NAMES.put("darkorange", 0xFFFF8C00); + SVG_COLOR_NAMES.put("darkorchid", 0xFF9932CC); + SVG_COLOR_NAMES.put("darkred", 0xFF8B0000); + SVG_COLOR_NAMES.put("darksalmon", 0xFFE9967A); + SVG_COLOR_NAMES.put("darkseagreen", 0xFF8FBC8F); + SVG_COLOR_NAMES.put("darkslateblue", 0xFF483D8B); + SVG_COLOR_NAMES.put("darkslategray", 0xFF2F4F4F); + SVG_COLOR_NAMES.put("darkslategrey", 0xFF2F4F4F); + SVG_COLOR_NAMES.put("darkturquoise", 0xFF00CED1); + SVG_COLOR_NAMES.put("darkviolet", 0xFF9400D3); + SVG_COLOR_NAMES.put("deeppink", 0xFFFF1493); + SVG_COLOR_NAMES.put("deepskyblue", 0xFF00BFFF); + SVG_COLOR_NAMES.put("dimgray", 0xFF696969); + SVG_COLOR_NAMES.put("dimgrey", 0xFF696969); + SVG_COLOR_NAMES.put("dodgerblue", 0xFF1E90FF); + SVG_COLOR_NAMES.put("firebrick", 0xFFB22222); + SVG_COLOR_NAMES.put("floralwhite", 0xFFFFFAF0); + SVG_COLOR_NAMES.put("forestgreen", 0xFF228B22); + SVG_COLOR_NAMES.put("fuchsia", 0xFFFF00FF); + SVG_COLOR_NAMES.put("gainsboro", 0xFFDCDCDC); + SVG_COLOR_NAMES.put("ghostwhite", 0xFFF8F8FF); + SVG_COLOR_NAMES.put("gold", 0xFFFFD700); + SVG_COLOR_NAMES.put("goldenrod", 0xFFDAA520); + SVG_COLOR_NAMES.put("gray", 0xFF808080); + SVG_COLOR_NAMES.put("grey", 0xFF808080); + SVG_COLOR_NAMES.put("green", 0xFF008000); + SVG_COLOR_NAMES.put("greenyellow", 0xFFADFF2F); + SVG_COLOR_NAMES.put("honeydew", 0xFFF0FFF0); + SVG_COLOR_NAMES.put("hotpink", 0xFFFF69B4); + SVG_COLOR_NAMES.put("indianred", 0xFFCD5C5C); + SVG_COLOR_NAMES.put("indigo", 0xFF4B0082); + SVG_COLOR_NAMES.put("ivory", 0xFFFFFFF0); + SVG_COLOR_NAMES.put("khaki", 0xFFF0E68C); + SVG_COLOR_NAMES.put("lavender", 0xFFE6E6FA); + SVG_COLOR_NAMES.put("lavenderblush", 0xFFFFF0F5); + SVG_COLOR_NAMES.put("lawngreen", 0xFF7CFC00); + SVG_COLOR_NAMES.put("lemonchiffon", 0xFFFFFACD); + SVG_COLOR_NAMES.put("lightblue", 0xFFADD8E6); + SVG_COLOR_NAMES.put("lightcoral", 0xFFF08080); + SVG_COLOR_NAMES.put("lightcyan", 0xFFE0FFFF); + SVG_COLOR_NAMES.put("lightgoldenrodyellow", 0xFFFAFAD2); + SVG_COLOR_NAMES.put("lightgray", 0xFFD3D3D3); + SVG_COLOR_NAMES.put("lightgreen", 0xFF90EE90); + SVG_COLOR_NAMES.put("lightgrey", 0xFFD3D3D3); + SVG_COLOR_NAMES.put("lightpink", 0xFFFFB6C1); + SVG_COLOR_NAMES.put("lightsalmon", 0xFFFFA07A); + SVG_COLOR_NAMES.put("lightseagreen", 0xFF20B2AA); + SVG_COLOR_NAMES.put("lightskyblue", 0xFF87CEFA); + SVG_COLOR_NAMES.put("lightslategray", 0xFF778899); + SVG_COLOR_NAMES.put("lightslategrey", 0xFF778899); + SVG_COLOR_NAMES.put("lightsteelblue", 0xFFB0C4DE); + SVG_COLOR_NAMES.put("lightyellow", 0xFFFFFFE0); + SVG_COLOR_NAMES.put("lime", 0xFF00FF00); + SVG_COLOR_NAMES.put("limegreen", 0xFF32CD32); + SVG_COLOR_NAMES.put("linen", 0xFFFAF0E6); + SVG_COLOR_NAMES.put("magenta", 0xFFFF00FF); + SVG_COLOR_NAMES.put("maroon", 0xFF800000); + SVG_COLOR_NAMES.put("mediumaquamarine", 0xFF66CDAA); + SVG_COLOR_NAMES.put("mediumblue", 0xFF0000CD); + SVG_COLOR_NAMES.put("mediumorchid", 0xFFBA55D3); + SVG_COLOR_NAMES.put("mediumpurple", 0xFF9370DB); + SVG_COLOR_NAMES.put("mediumseagreen", 0xFF3CB371); + SVG_COLOR_NAMES.put("mediumslateblue", 0xFF7B68EE); + SVG_COLOR_NAMES.put("mediumspringgreen", 0xFF00FA9A); + SVG_COLOR_NAMES.put("mediumturquoise", 0xFF48D1CC); + SVG_COLOR_NAMES.put("mediumvioletred", 0xFFC71585); + SVG_COLOR_NAMES.put("midnightblue", 0xFF191970); + SVG_COLOR_NAMES.put("mintcream", 0xFFF5FFFA); + SVG_COLOR_NAMES.put("mistyrose", 0xFFFFE4E1); + SVG_COLOR_NAMES.put("moccasin", 0xFFFFE4B5); + SVG_COLOR_NAMES.put("navajowhite", 0xFFFFDEAD); + SVG_COLOR_NAMES.put("navy", 0xFF000080); + SVG_COLOR_NAMES.put("oldlace", 0xFFFDF5E6); + SVG_COLOR_NAMES.put("olive", 0xFF808000); + SVG_COLOR_NAMES.put("olivedrab", 0xFF6B8E23); + SVG_COLOR_NAMES.put("orange", 0xFFFFA500); + SVG_COLOR_NAMES.put("orangered", 0xFFFF4500); + SVG_COLOR_NAMES.put("orchid", 0xFFDA70D6); + SVG_COLOR_NAMES.put("palegoldenrod", 0xFFEEE8AA); + SVG_COLOR_NAMES.put("palegreen", 0xFF98FB98); + SVG_COLOR_NAMES.put("paleturquoise", 0xFFAFEEEE); + SVG_COLOR_NAMES.put("palevioletred", 0xFFDB7093); + SVG_COLOR_NAMES.put("papayawhip", 0xFFFFEFD5); + SVG_COLOR_NAMES.put("peachpuff", 0xFFFFDAB9); + SVG_COLOR_NAMES.put("peru", 0xFFCD853F); + SVG_COLOR_NAMES.put("pink", 0xFFFFC0CB); + SVG_COLOR_NAMES.put("plum", 0xFFDDA0DD); + SVG_COLOR_NAMES.put("powderblue", 0xFFB0E0E6); + SVG_COLOR_NAMES.put("purple", 0xFF800080); + SVG_COLOR_NAMES.put("red", 0xFFFF0000); + SVG_COLOR_NAMES.put("rosybrown", 0xFFBC8F8F); + SVG_COLOR_NAMES.put("royalblue", 0xFF4169E1); + SVG_COLOR_NAMES.put("saddlebrown", 0xFF8B4513); + SVG_COLOR_NAMES.put("salmon", 0xFFFA8072); + SVG_COLOR_NAMES.put("sandybrown", 0xFFF4A460); + SVG_COLOR_NAMES.put("seagreen", 0xFF2E8B57); + SVG_COLOR_NAMES.put("seashell", 0xFFFFF5EE); + SVG_COLOR_NAMES.put("sienna", 0xFFA0522D); + SVG_COLOR_NAMES.put("silver", 0xFFC0C0C0); + SVG_COLOR_NAMES.put("skyblue", 0xFF87CEEB); + SVG_COLOR_NAMES.put("slateblue", 0xFF6A5ACD); + SVG_COLOR_NAMES.put("slategray", 0xFF708090); + SVG_COLOR_NAMES.put("slategrey", 0xFF708090); + SVG_COLOR_NAMES.put("snow", 0xFFFFFAFA); + SVG_COLOR_NAMES.put("springgreen", 0xFF00FF7F); + SVG_COLOR_NAMES.put("steelblue", 0xFF4682B4); + SVG_COLOR_NAMES.put("tan", 0xFFD2B48C); + SVG_COLOR_NAMES.put("teal", 0xFF008080); + SVG_COLOR_NAMES.put("thistle", 0xFFD8BFD8); + SVG_COLOR_NAMES.put("tomato", 0xFFFF6347); + SVG_COLOR_NAMES.put("turquoise", 0xFF40E0D0); + SVG_COLOR_NAMES.put("violet", 0xFFEE82EE); + SVG_COLOR_NAMES.put("wheat", 0xFFF5DEB3); + SVG_COLOR_NAMES.put("white", 0xFFFFFFFF); + SVG_COLOR_NAMES.put("whitesmoke", 0xFFF5F5F5); + SVG_COLOR_NAMES.put("yellow", 0xFFFFFF00); + SVG_COLOR_NAMES.put("yellowgreen", 0xFF9ACD32); + // Nonstandard: + SVG_COLOR_NAMES.put("transparent", 0xFFFFFFFF); + } + + /** + * CSS Stylesheet from Javax, to parse color values. + */ + private static final StyleSheet colorLookupStylesheet = new StyleSheet(); + + /** + * Format a double according to the SVG specs. + * + * @param x number to format + * @return String representation + */ + public static String fmt(double x) { + return FMT.format(x); + } + + /** + * Create a SVG element in appropriate namespace + * + * @param document containing document + * @param name node name + * @return new SVG element. + */ + public static Element svgElement(Document document, String name) { + return document.createElementNS(SVGConstants.SVG_NAMESPACE_URI, name); + } + + /** + * Set a SVG attribute + * + * @param el element + * @param name attribute name + * @param d double value + */ + public static void setAtt(Element el, String name, double d) { + el.setAttribute(name, fmt(d)); + } + + /** + * Set a SVG attribute + * + * @param el element + * @param name attribute name + * @param d integer value + */ + public static void setAtt(Element el, String name, int d) { + el.setAttribute(name, Integer.toString(d)); + } + + /** + * Set a SVG attribute + * + * @param el element + * @param name attribute name + * @param d string value + */ + public static void setAtt(Element el, String name, String d) { + el.setAttribute(name, d); + } + + /** + * Set a SVG style attribute + * + * @param el element + * @param d style value + */ + public static void setStyle(Element el, String d) { + el.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, d); + } + + /** + * Set the CSS class of an Element. See also {@link #addCSSClass} and + * {@link #removeCSSClass}. + * + * @param e Element + * @param cssclass class to set. + */ + public static void setCSSClass(Element e, String cssclass) { + setAtt(e, SVGConstants.SVG_CLASS_ATTRIBUTE, cssclass); + } + + /** + * Add a CSS class to an Element. + * + * @param e Element + * @param cssclass class to add. + */ + public static void addCSSClass(Element e, String cssclass) { + String oldval = e.getAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE); + if(oldval == null || oldval.length() == 0) { + setAtt(e, SVGConstants.SVG_CLASS_ATTRIBUTE, cssclass); + return; + } + String[] classes = oldval.split(" "); + for(String c : classes) { + if(c.equals(cssclass)) { + return; + } + } + setAtt(e, SVGConstants.SVG_CLASS_ATTRIBUTE, oldval + " " + cssclass); + } + + /** + * Remove a CSS class from an Element. + * + * @param e Element + * @param cssclass class to remove. + */ + public static void removeCSSClass(Element e, String cssclass) { + String oldval = e.getAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE); + if(oldval == null) { + return; + } + String[] classes = oldval.split(" "); + if(classes.length == 1) { + if(cssclass.equals(classes[0])) { + e.removeAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE); + } + } + else if(classes.length == 2) { + if(cssclass.equals(classes[0])) { + if(cssclass.equals(classes[1])) { + e.removeAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE); + } + else { + e.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, classes[1]); + } + } + else if(cssclass.equals(classes[1])) { + e.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, classes[0]); + } + } + else { + StringBuilder joined = new StringBuilder(); + for(String c : classes) { + if(!c.equals(cssclass)) { + if(joined.length() > 0) { + joined.append(' '); + } + joined.append(c); + } + } + e.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, joined.toString()); + } + } + + /** + * Make a new CSS style element for the given Document. + * + * @param document document (factory) + * @return new CSS style element. + */ + public static Element makeStyleElement(Document document) { + Element style = SVGUtil.svgElement(document, SVGConstants.SVG_STYLE_TAG); + style.setAttribute(SVGConstants.SVG_TYPE_ATTRIBUTE, SVGConstants.CSS_MIME_TYPE); + return style; + } + + /** + * Create a SVG rectangle element. + * + * @param document document to create in (factory) + * @param x X coordinate + * @param y Y coordinate + * @param w Width + * @param h Height + * @return new element + */ + public static Element svgRect(Document document, double x, double y, double w, double h) { + Element rect = SVGUtil.svgElement(document, SVGConstants.SVG_RECT_TAG); + SVGUtil.setAtt(rect, SVGConstants.SVG_X_ATTRIBUTE, x); + SVGUtil.setAtt(rect, SVGConstants.SVG_Y_ATTRIBUTE, y); + SVGUtil.setAtt(rect, SVGConstants.SVG_WIDTH_ATTRIBUTE, w); + SVGUtil.setAtt(rect, SVGConstants.SVG_HEIGHT_ATTRIBUTE, h); + return rect; + } + + /** + * Create a SVG circle element. + * + * @param document document to create in (factory) + * @param cx center X + * @param cy center Y + * @param r radius + * @return new element + */ + public static Element svgCircle(Document document, double cx, double cy, double r) { + Element circ = SVGUtil.svgElement(document, SVGConstants.SVG_CIRCLE_TAG); + SVGUtil.setAtt(circ, SVGConstants.SVG_CX_ATTRIBUTE, cx); + SVGUtil.setAtt(circ, SVGConstants.SVG_CY_ATTRIBUTE, cy); + SVGUtil.setAtt(circ, SVGConstants.SVG_R_ATTRIBUTE, r); + return circ; + } + + /** + * Create a SVG line element. Do not confuse this with path elements. + * + * @param document document to create in (factory) + * @param x1 first point x + * @param y1 first point y + * @param x2 second point x + * @param y2 second point y + * @return new element + */ + public static Element svgLine(Document document, double x1, double y1, double x2, double y2) { + Element line = SVGUtil.svgElement(document, SVGConstants.SVG_LINE_TAG); + SVGUtil.setAtt(line, SVGConstants.SVG_X1_ATTRIBUTE, x1); + SVGUtil.setAtt(line, SVGConstants.SVG_Y1_ATTRIBUTE, y1); + SVGUtil.setAtt(line, SVGConstants.SVG_X2_ATTRIBUTE, x2); + SVGUtil.setAtt(line, SVGConstants.SVG_Y2_ATTRIBUTE, y2); + return line; + } + + /** + * Create a SVG text element. + * + * @param document document to create in (factory) + * @param x first point x + * @param y first point y + * @param text Content of text element. + * @return New text element. + */ + public static Element svgText(Document document, double x, double y, String text) { + Element elem = SVGUtil.svgElement(document, SVGConstants.SVG_TEXT_TAG); + SVGUtil.setAtt(elem, SVGConstants.SVG_X_ATTRIBUTE, x); + SVGUtil.setAtt(elem, SVGConstants.SVG_Y_ATTRIBUTE, y); + elem.setTextContent(text); + return elem; + } + + /** + * Draw a simple "please wait" icon (in-progress) as placeholder for running + * renderings. + * + * @param document Document. + * @param x Left + * @param y Top + * @param w Width + * @param h Height + * @return New element (currently a {@link SVGConstants#SVG_PATH_TAG}) + */ + public static Element svgWaitIcon(Document document, double x, double y, double w, double h) { + Element g = SVGUtil.svgElement(document, SVGConstants.SVG_G_TAG); + setAtt(g, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "translate(" + x + " " + y + ") scale(" + w + " " + h + ")"); + Element thro = SVGUtil.svgElement(document, SVGConstants.SVG_PATH_TAG); + setAtt(thro, SVGConstants.SVG_D_ATTRIBUTE, THROBBER_PATH); + setStyle(thro, THROBBER_STYLE); + Element anim = SVGUtil.svgElement(document, SVGConstants.SVG_ANIMATE_TRANSFORM_TAG); + setAtt(anim, SVGConstants.SVG_ATTRIBUTE_NAME_ATTRIBUTE, SVGConstants.SVG_TRANSFORM_ATTRIBUTE); + setAtt(anim, SVGConstants.SVG_ATTRIBUTE_TYPE_ATTRIBUTE, "XML"); + setAtt(anim, SVGConstants.SVG_TYPE_ATTRIBUTE, SVGConstants.SVG_ROTATE_ATTRIBUTE); + setAtt(anim, SVGConstants.SVG_FROM_ATTRIBUTE, "0 .5 .5"); + setAtt(anim, SVGConstants.SVG_TO_ATTRIBUTE, "360 .5 .5"); + setAtt(anim, SVGConstants.SVG_BEGIN_ATTRIBUTE, fmt(Math.random() * 2) + "s"); + setAtt(anim, SVGConstants.SVG_DUR_ATTRIBUTE, "2s"); + setAtt(anim, SVGConstants.SVG_REPEAT_COUNT_ATTRIBUTE, "indefinite"); + setAtt(anim, SVGConstants.SVG_FILL_ATTRIBUTE, "freeze"); + thro.appendChild(anim); + g.appendChild(thro); + return g; + } + + /** + * Convert a color name from SVG syntax to an AWT color object. + * + * @param str Color name + * @return Color value + */ + public static Color stringToColor(String str) { + int icol = SVG_COLOR_NAMES.get(str.toLowerCase()); + if(icol != NO_VALUE) { + return new Color(icol, false); + } + return colorLookupStylesheet.stringToColor(str); + } + + /** + * Convert a color name from an AWT color object to CSS syntax + * + * Note: currently only RGB (from ARGB order) are supported. + * + * @param col Color value + * @return Color string + */ + public static String colorToString(Color col) { + return colorToString(col.getRGB()); + } + + /** + * Convert a color name from an integer RGB color to CSS syntax + * + * Note: currently only RGB (from ARGB order) are supported. The alpha channel + * will be ignored. + * + * @param col Color value + * @return Color string + */ + public static String colorToString(int col) { + final char[] buf = new char[] { '#', 'X', 'X', 'X', 'X', 'X', 'X' }; + for(int i = 6; i > 0; i--) { + final int v = (col & 0xF); + buf[i] = (char) ((v < 10) ? ('0' + v) : ('a' + v - 10)); + col >>>= 4; + } + return new String(buf); + } + + /** + * Make a transform string to add margins + * + * @param owidth Width of outer (embedding) canvas + * @param oheight Height of outer (embedding) canvas + * @param iwidth Width of inner (embedded) canvas + * @param iheight Height of inner (embedded) canvas + * @param lmargin Left margin (in inner canvas' units) + * @param tmargin Top margin (in inner canvas' units) + * @param rmargin Right margin (in inner canvas' units) + * @param bmargin Bottom margin (in inner canvas' units) + * @return Transform string + */ + public static String makeMarginTransform(double owidth, double oheight, double iwidth, double iheight, double lmargin, double tmargin, double rmargin, double bmargin) { + double swidth = iwidth + lmargin + rmargin; + double sheight = iheight + tmargin + bmargin; + double scale = Math.max(swidth / owidth, sheight / oheight); + double offx = (scale * owidth - swidth) * .5 + lmargin; + double offy = (scale * oheight - sheight) * .5 + tmargin; + return "scale(" + fmt(1 / scale) + ") translate(" + fmt(offx) + " " + fmt(offy) + ")"; + } + + /** + * Make a transform string to add margins + * + * @param owidth Width of outer (embedding) canvas + * @param oheight Height of outer (embedding) canvas + * @param iwidth Width of inner (embedded) canvas + * @param iheight Height of inner (embedded) canvas + * @param xmargin Left and right margin (in inner canvas' units) + * @param ymargin Top and bottom margin (in inner canvas' units) + * @return Transform string + */ + public static String makeMarginTransform(double owidth, double oheight, double iwidth, double iheight, double xmargin, double ymargin) { + return makeMarginTransform(owidth, oheight, iwidth, iheight, xmargin, ymargin, xmargin, ymargin); + } + + /** + * Make a transform string to add margins + * + * @param owidth Width of outer (embedding) canvas + * @param oheight Height of outer (embedding) canvas + * @param iwidth Width of inner (embedded) canvas + * @param iheight Height of inner (embedded) canvas + * @param margin Margin (in inner canvas' units) + * @return Transform string + */ + public static String makeMarginTransform(double owidth, double oheight, double iwidth, double iheight, double margin) { + return makeMarginTransform(owidth, oheight, iwidth, iheight, margin, margin, margin, margin); + } + + /** + * Convert the coordinates of an DOM Event from screen into element + * coordinates. + * + * @param doc Document context + * @param tag Element containing the coordinate system + * @param evt Event to interpret + * @return coordinates + */ + public static SVGPoint elementCoordinatesFromEvent(Document doc, Element tag, Event evt) { + try { + DOMMouseEvent gnme = (DOMMouseEvent) evt; + SVGMatrix mat = ((SVGLocatable) tag).getScreenCTM(); + SVGMatrix imat = mat.inverse(); + SVGPoint cPt = ((SVGDocument) doc).getRootElement().createSVGPoint(); + cPt.setX(gnme.getClientX()); + cPt.setY(gnme.getClientY()); + return cPt.matrixTransform(imat); + } + catch(Exception e) { + LoggingUtil.warning("Error getting coordinates from SVG event.", e); + return null; + } + } + + /** + * Remove last child of an element, when present + * + * @param tag Parent + */ + public static void removeLastChild(Element tag) { + final Node last = tag.getLastChild(); + if(last != null) { + tag.removeChild(last); + } + } + + /** + * Remove an element from its parent, if defined. + * + * @param elem Element to remove + */ + public static void removeFromParent(Element elem) { + if(elem != null && elem.getParentNode() != null) { + elem.getParentNode().removeChild(elem); + } + } + + /** + * Create a circle segment. + * + * @param svgp Plot to draw to + * @param centerx Center X position + * @param centery Center Y position + * @param angleStart Starting angle + * @param angleDelta Angle delta + * @param innerRadius inner radius + * @param outerRadius outer radius + * @return SVG element representing this circle segment + */ + public static Element svgCircleSegment(SVGPlot svgp, double centerx, double centery, double angleStart, double angleDelta, double innerRadius, double outerRadius) { + double sin1st = Math.sin(angleStart); + double cos1st = MathUtil.sinToCos(angleStart, sin1st); + + double sin2nd = Math.sin(angleStart + angleDelta); + double cos2nd = MathUtil.sinToCos(angleStart + angleDelta, sin2nd); + + double inner1stx = centerx + (innerRadius * sin1st); + double inner1sty = centery - (innerRadius * cos1st); + double outer1stx = centerx + (outerRadius * sin1st); + double outer1sty = centery - (outerRadius * cos1st); + + double inner2ndx = centerx + (innerRadius * sin2nd); + double inner2ndy = centery - (innerRadius * cos2nd); + double outer2ndx = centerx + (outerRadius * sin2nd); + double outer2ndy = centery - (outerRadius * cos2nd); + + double largeArc = 0; + if(angleDelta >= Math.PI) { + largeArc = 1; + } + + SVGPath path = new SVGPath(inner1stx, inner1sty); + path.lineTo(outer1stx, outer1sty); + path.ellipticalArc(outerRadius, outerRadius, 0, largeArc, 1, outer2ndx, outer2ndy); + path.lineTo(inner2ndx, inner2ndy); + if(innerRadius > 0) { + path.ellipticalArc(innerRadius, innerRadius, 0, largeArc, 0, inner1stx, inner1sty); + } + + return path.makeElement(svgp); + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java new file mode 100644 index 00000000..17033ef8 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java @@ -0,0 +1,161 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.concurrent.ConcurrentLinkedQueue; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; + +/** + * Class to handle updates to an SVG plot, in particular when used in an Apache + * Batik UI. + * + * @author Erich Schubert + * + * @apiviz.has Runnable + * @apiviz.uses UpdateSynchronizer + */ +public class UpdateRunner { + /** + * Owner/Synchronization object + */ + private Object sync; + + /** + * The queue of pending updates + */ + final private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>(); + + /** + * Synchronizer that can block events from being executed right away. + */ + private UpdateSynchronizer synchronizer = null; + + /** + * Construct a new update handler + * + * @param sync Object to synchronize on + */ + protected UpdateRunner(Object sync) { + this.sync = sync; + } + + /** + * Add a new update to run at any appropriate time. + * + * @param r New runnable to perform the update + */ + public void invokeLater(Runnable r) { + queue.add(r); + synchronized(this) { + if(synchronizer == null) { + runQueue(); + } + else { + synchronizer.activate(); + } + } + } + + /** + * Run the processing queue now. This should usually be only invoked by the + * UpdateSynchronizer + */ + public void runQueue() { + synchronized(sync) { + while(!queue.isEmpty()) { + Runnable r = queue.poll(); + if(r != null) { + try { + r.run(); + } + catch(Exception e) { + // Alternatively, we could allow the specification of exception + // handlers for each runnable in the API. For now we'll just log. + // TODO: handle exceptions here better! + LoggingUtil.exception(e); + } + } + else { + LoggingUtil.warning("Tried to run a 'null' Object."); + } + } + } + } + + /** + * Clear queue. For shutdown! + */ + public synchronized void clear() { + queue.clear(); + } + + /** + * Check whether the queue is empty. + * + * @return queue status + */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** + * Set a new update synchronizer. + * + * @param newsync Update synchronizer + */ + public synchronized void synchronizeWith(UpdateSynchronizer newsync) { + // LoggingUtil.warning("Synchronizing: " + sync + " " + newsync, new + // Throwable()); + if(synchronizer == newsync) { + LoggingUtil.warning("Double-synced to the same plot!", new Throwable()); + return; + } + if(synchronizer != null) { + LoggingUtil.warning("Attempting to synchronize to more than one synchronizer."); + return; + } + synchronizer = newsync; + newsync.addUpdateRunner(this); + } + + /** + * Remove an update synchronizer + * + * @param oldsync Update synchronizer to remove + */ + public synchronized void unsynchronizeWith(UpdateSynchronizer oldsync) { + if(synchronizer == null) { + LoggingUtil.warning("Warning: was not synchronized."); + return; + } + if(synchronizer != oldsync) { + LoggingUtil.warning("Warning: was synchronized differently!"); + return; + } + // LoggingUtil.warning("Unsynchronizing: " + sync + " " + oldsync); + synchronizer = null; + runQueue(); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java new file mode 100644 index 00000000..30eaa304 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java @@ -0,0 +1,46 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + + +/** + * API to synchronize updates + * + * @author Erich Schubert + * + * @apiviz.has UpdateRunner + */ +public interface UpdateSynchronizer { + /** + * This method is called whenever a new pending event was added. + */ + void activate(); + + /** + * Set an update runner to use. + * + * @param updateRunner + */ + void addUpdateRunner(UpdateRunner updateRunner); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java new file mode 100644 index 00000000..434bf240 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java @@ -0,0 +1,155 @@ +package de.lmu.ifi.dbs.elki.visualization.svg; +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.List; + +import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D; +import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D.Triangle; +import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath; +import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; + +/** + * Draw the Voronoi cells + * + * @author Robert Rödler + * @author Erich Schubert + * + * @apiviz.uses de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D.Triangle + * @apiviz.uses Projection2D + */ +public class VoronoiDraw { + /** + * Draw the Delaunay triangulation. + * + * @param proj Projection + * @param delaunay Triangulation + * @param means Means + * @return Path + */ + public static SVGPath drawDelaunay(Projection2D proj, List<SweepHullDelaunay2D.Triangle> delaunay, List<double[]> means) { + final SVGPath path = new SVGPath(); + for(SweepHullDelaunay2D.Triangle del : delaunay) { + path.moveTo(proj.fastProjectDataToRenderSpace(means.get(del.a))); + path.drawTo(proj.fastProjectDataToRenderSpace(means.get(del.b))); + path.drawTo(proj.fastProjectDataToRenderSpace(means.get(del.c))); + path.close(); + } + return path; + } + + /** + * Draw a Voronoi diagram + * + * @param proj Projection + * @param delaunay Delaunay triangulation + * @param means Cluster means + * @return SVG path + */ + public static SVGPath drawVoronoi(Projection2D proj, List<SweepHullDelaunay2D.Triangle> delaunay, List<double[]> means) { + final SVGPath path = new SVGPath(); + CanvasSize viewport = proj.estimateViewport(); + for(int i = 0; i < delaunay.size(); i++) { + SweepHullDelaunay2D.Triangle del = delaunay.get(i); + final double[] projcx = proj.fastProjectDataToRenderSpace(del.m.getArrayRef()); + if(del.ab > i) { + Triangle oth = delaunay.get(del.ab); + path.moveTo(projcx); + path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef())); + } + else if(del.ab < 0) { + double[] dirv = VMath.minus(means.get(del.a), means.get(del.b)); + VMath.rotate90Equals(dirv); + double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv); + final double factor = viewport.continueToMargin(projcx, dir); + if(factor > 0) { + path.moveTo(projcx); + path.relativeLineTo(factor * dir[0], factor * dir[1]); + } + } + + if(del.bc > i) { + Triangle oth = delaunay.get(del.bc); + path.moveTo(projcx); + path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef())); + } + else if(del.bc < 0) { + double[] dirv = VMath.minus(means.get(del.b), means.get(del.c)); + VMath.rotate90Equals(dirv); + double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv); + final double factor = viewport.continueToMargin(projcx, dir); + if(factor > 0) { + path.moveTo(projcx); + path.relativeLineTo(factor * dir[0], factor * dir[1]); + } + } + + if(del.ca > i) { + Triangle oth = delaunay.get(del.ca); + path.moveTo(projcx); + path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef())); + } + else if(del.ca < 0) { + double[] dirv = VMath.minus(means.get(del.c), means.get(del.a)); + VMath.rotate90Equals(dirv); + double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv); + final double factor = viewport.continueToMargin(projcx, dir); + if(factor > 0) { + path.moveTo(projcx); + path.relativeLineTo(factor * dir[0], factor * dir[1]); + } + } + } + return path; + } + + /** + * Fake Voronoi diagram. For two means only + * + * @param proj Projection + * @param means Mean vectors + * @return SVG path + */ + public static SVGPath drawFakeVoronoi(Projection2D proj, List<double[]> means) { + CanvasSize viewport = proj.estimateViewport(); + final SVGPath path = new SVGPath(); + // Difference + final double[] dirv = VMath.minus(means.get(1), means.get(0)); + VMath.rotate90Equals(dirv); + double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv); + // Mean + final double[] mean = VMath.plus(means.get(0), means.get(1)); + VMath.timesEquals(mean, 0.5); + double[] projmean = proj.fastProjectDataToRenderSpace(mean); + + double factor = viewport.continueToMargin(projmean, dir); + path.moveTo(projmean[0] + factor * dir[0], projmean[1] + factor * dir[1]); + // Inverse direction: + dir[0] *= -1; + dir[1] *= -1; + factor = viewport.continueToMargin(projmean, dir); + path.drawTo(projmean[0] + factor * dir[0], projmean[1] + factor * dir[1]); + return path; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java new file mode 100755 index 00000000..2ea11699 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Base SVG functionality (generation, markers, thumbnails, export, ...).</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.svg;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisFactory.java new file mode 100644 index 00000000..841ea83f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisFactory.java @@ -0,0 +1,73 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs.ThumbnailVisualization; + +/** + * Abstract superclass for Visualizers (aka: Visualization Factories). + * + * @author Remigius Wojdanowski + * + * @apiviz.uses ThumbnailVisualization oneway - - «create» + * @apiviz.excludeSubtypes + */ +public abstract class AbstractVisFactory implements VisFactory { + /** + * Constructor. + */ + protected AbstractVisFactory() { + super(); + } + + @Override + public Visualization makeVisualizationOrThumbnail(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj, int thumbsize) { + if(width <= 0 || height <= 0) { + LoggingUtil.warning("Cannot generate visualization of 0 size.", new Throwable()); + return null; + } + if(allowThumbnails(task)) { + return new ThumbnailVisualization(this, task, plot, width, height, proj, thumbsize); + } + return makeVisualization(task, plot, width, height, proj); + } + + @Override + abstract public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj); + + /** + * Test whether to do a thumbnail or a full rendering. + * + * Override this with "false" to disable thumbnails! + * + * @param task Task requested + */ + public boolean allowThumbnails(VisualizationTask task) { + return true; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisualization.java new file mode 100644 index 00000000..49e749c0 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisualization.java @@ -0,0 +1,214 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreEvent; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultListener; +import de.lmu.ifi.dbs.elki.result.SamplingResult; +import de.lmu.ifi.dbs.elki.result.SelectionResult; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizationListener; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; + +/** + * Abstract base class for visualizations. + * + * @author Erich Schubert + * + * @apiviz.excludeSubtypes + */ +public abstract class AbstractVisualization implements Visualization, ResultListener, VisualizationListener, DataStoreListener { + /** + * The visualization task we do. + */ + protected final VisualizationTask task; + + /** + * Our context + */ + protected final VisualizerContext context; + + /** + * The plot we are attached to + */ + protected final VisualizationPlot svgp; + + /** + * Layer storage + */ + protected Element layer; + + /** + * Width + */ + private double width; + + /** + * Height + */ + private double height; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + */ + public AbstractVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height) { + super(); + this.task = task; + this.context = task.getContext(); + this.svgp = plot; + this.width = width; + this.height = height; + this.layer = null; + // Note: we do not auto-add listeners, as we don't know what kind of + // listeners a visualizer needs, and the visualizer might need to do some + // initialization first + } + + /** + * Add the listeners according to the mask. + */ + protected void addListeners() { + // Listen for result changes, including the one we monitor + context.addResultListener(this); + context.addVisualizationListener(this); + // Listen for database events only when needed. + if(task.updateOnAny(VisualizationTask.ON_DATA)) { + context.addDataStoreListener(this); + } + } + + @Override + public void destroy() { + // Always unregister listeners, as this is easy to forget otherwise + // TODO: remove destroy() overrides that are redundant? + context.removeResultListener(this); + context.removeVisualizationListener(this); + context.removeDataStoreListener((DataStoreListener) this); + } + + @Override + public Element getLayer() { + if(layer == null) { + incrementalRedraw(); + } + return layer; + } + + /** + * Get the width + * + * @return the width + */ + protected double getWidth() { + return width; + } + + /** + * Get the height + * + * @return the height + */ + protected double getHeight() { + return height; + } + + /** + * Redraw the visualization (maybe incremental). + * + * Optional - by default, it will do a full redraw, which often is faster! + */ + @Override + public void incrementalRedraw() { + Element oldcontainer = null; + if(layer != null && layer.hasChildNodes()) { + oldcontainer = layer; + // Shallow clone: + layer = (Element) layer.cloneNode(false); + } + fullRedraw(); + if(oldcontainer != null && oldcontainer.getParentNode() != null) { + oldcontainer.getParentNode().replaceChild(layer, oldcontainer); + } + } + + @Override + public abstract void fullRedraw(); + + @Override + public void resultAdded(Result child, Result parent) { + // Ignore by default + } + + @Override + public void resultChanged(Result current) { + // Default is to redraw when the result we are attached to changed. + if(task.getResult() == current) { + svgp.requestRedraw(this.task, this); + return; + } + if(task.updateOnAny(VisualizationTask.ON_SELECTION) && current instanceof SelectionResult) { + svgp.requestRedraw(this.task, this); + return; + } + if(task.updateOnAny(VisualizationTask.ON_SAMPLE) && current instanceof SamplingResult) { + svgp.requestRedraw(this.task, this); + return; + } + } + + @Override + public void resultRemoved(Result child, Result parent) { + // Ignore by default. + // TODO: auto-remove if parent result is removed? + } + + @Override + public void visualizationChanged(VisualizationItem item) { + if(task.getResult() == item) { + svgp.requestRedraw(this.task, this); + return; + } + if(task.updateOnAny(VisualizationTask.ON_STYLEPOLICY) && item instanceof StylingPolicy) { + svgp.requestRedraw(this.task, this); + return; + } + } + + @Override + public void contentChanged(DataStoreEvent e) { + svgp.requestRedraw(this.task, this); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/StaticVisualizationInstance.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/StaticVisualizationInstance.java new file mode 100644 index 00000000..b89720fe --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/StaticVisualizationInstance.java @@ -0,0 +1,60 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; + +/** + * Static visualization + * + * @author Erich Schubert + */ +public class StaticVisualizationInstance extends AbstractVisualization { + /** + * Unchanging precomputed visualization. + * + * @param task Task to visualize + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param element Element containing the resulting visualization + */ + public StaticVisualizationInstance(VisualizationTask task, VisualizationPlot plot, double width, double height, Element element) { + super(task, plot, width, height); + this.layer = element; + } + + @Override + public void incrementalRedraw() { + // Do nothing - we keep our static layer + } + + @Override + public void fullRedraw() { + // Do nothing - we keep our static layer + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/VisFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/VisFactory.java new file mode 100644 index 00000000..bf30fe8f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/VisFactory.java @@ -0,0 +1,77 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers; +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; + +/** + * Defines the requirements for a visualizer. <br> + * Note: Any implementation is supposed to provide a constructor without + * parameters (default constructor) to be used for parameterization. + * + * @author Remigius Wojdanowski + * + * @apiviz.landmark + * @apiviz.stereotype factory + * @apiviz.uses Visualization - - «create» + * @apiviz.uses VisualizationTask - - «create» + */ +public interface VisFactory extends VisualizationProcessor { + /** + * Add visualizers for the given result (tree) to the context. + * + * @param context Visualization context + * @param start Result to process + */ + @Override + void processNewResult(VisualizerContext context, Object start); + + /** + * Produce a visualization instance for the given task + * + * @param task Visualization task + * @param plot Plot + * @param width Width + * @param height Height + * @param proj Projection + * @return Visualization + */ + Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj); + + /** + * Produce a visualization instance for the given task that may use thumbnails + * + * @param task Visualization task + * @param plot Plot + * @param width Width + * @param height Height + * @param proj Projection + * @param thumbsize Thumbnail size + * @return Visualization + */ + Visualization makeVisualizationOrThumbnail(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj, int thumbsize); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/Visualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/Visualization.java new file mode 100644 index 00000000..ee355e4a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/Visualization.java @@ -0,0 +1,61 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +/** + * Base class for a materialized Visualization. + * + * @author Erich Schubert + * + * @apiviz.landmark + * @apiviz.has Element oneway + */ +public interface Visualization { + /** + * Get the SVG layer of the given visualization. + * + * @return layer + */ + Element getLayer(); + + /** + * Request an update of the visualization. + */ + void incrementalRedraw(); + + /** + * Request a full redrawing of the visualization. + */ + void fullRedraw(); + + /** + * Destroy the visualization. Called after the elements have been removed from + * the document. + * + * Implementations should remove their listeners etc. + */ + void destroy(); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/ClusterStyleAction.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/ClusterStyleAction.java new file mode 100644 index 00000000..7923509e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/ClusterStyleAction.java @@ -0,0 +1,116 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.actions; +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationMenuAction; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Actions to use clusterings for styling. + * + * @author Erich Schubert + */ +public class ClusterStyleAction extends AbstractVisFactory { + /** + * Constructor. + */ + public ClusterStyleAction() { + super(); + } + + @Override + public void processNewResult(final VisualizerContext context, Object start) { + Hierarchy.Iter<Clustering<?>> it = VisualizationTree.filterResults(context, start, Clustering.class); + for(; it.valid(); it.advance()) { + final Clustering<?> c = it.get(); + Hierarchy.Iter<SetStyleAction> it2 = VisualizationTree.filter(context, c, SetStyleAction.class); + if(it2.valid()) { + continue; // There already is a style button. + } + context.addVis(c, new SetStyleAction(c, context)); + } + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + throw new AbortException("Should never be called."); + } + + /** + * Action to use a clustering as {@link ClusterStylingPolicy}. + * + * @author Erich Schubert + */ + private static final class SetStyleAction implements VisualizationMenuAction { + /** + * Clustering to use + */ + private final Clustering<?> c; + + /** + * Visualization context. + */ + private final VisualizerContext context; + + /** + * Constructor. + * + * @param c Clustering + * @param context Context + */ + private SetStyleAction(Clustering<?> c, VisualizerContext context) { + this.c = c; + this.context = context; + } + + @Override + public void activate() { + context.setStylingPolicy(new ClusterStylingPolicy(c, context.getStyleLibrary())); + } + + @Override + public String getMenuName() { + return "Use as Styling Policy"; + } + + @Override + public boolean enabled() { + StylingPolicy sp = context.getStylingPolicy(); + if(!(sp instanceof ClusterStylingPolicy)) { + return true; + } + return ((ClusterStylingPolicy) sp).getClustering() != c; + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/package-info.java new file mode 100644 index 00000000..3d709ac1 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/package-info.java @@ -0,0 +1,27 @@ +/** + * Action-only "visualizers" that only produce menu entries. + */ + +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.actions;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/AbstractHistogramVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/AbstractHistogramVisualization.java new file mode 100644 index 00000000..34c648ba --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/AbstractHistogramVisualization.java @@ -0,0 +1,70 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.histogram; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection1D; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; + +/** + * One-dimensional projected visualization. + * + * @author Erich Schubert + * + * @apiviz.landmark + * @apiviz.has Projection1D + */ +public abstract class AbstractHistogramVisualization extends AbstractVisualization { + /** + * The current projection + */ + final protected Projection1D proj; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public AbstractHistogramVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height); + assert(proj instanceof Projection1D) : "Visualizer attached to wrong projection!"; + this.proj = (Projection1D) proj; + } + + @Override + public void resultChanged(Result current) { + super.resultChanged(current); + if(proj != null && current == proj) { + svgp.requestRedraw(this.task, this); + return; + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/ColoredHistogramVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/ColoredHistogramVisualizer.java new file mode 100644 index 00000000..80bbfeeb --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/ColoredHistogramVisualizer.java @@ -0,0 +1,437 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.histogram; +import java.util.Arrays; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.DoubleVector; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.DoubleMinMax; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.SamplingResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.histogram.DoubleArrayStaticHistogram; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +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.constraints.CommonConstraints; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.HistogramProjector; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.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 <http://www.gnu.org/licenses/>. + */ + +/** + * Generates a SVG-Element containing a histogram representing the distribution + * of the database's objects. + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class ColoredHistogramVisualizer extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String CNAME = "Histograms"; + + /** + * Settings + */ + protected Parameterizer settings; + + /** + * Number of bins to use in histogram. + */ + private static final int DEFAULT_BINS = 80; + + /** + * Constructor. + * + * @param settings Settings + */ + public ColoredHistogramVisualizer(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance<DoubleVector>(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNew(context, start, HistogramProjector.class, // + new VisualizationTree.Handler1<HistogramProjector<?>>() { + @Override + public void process(VisualizerContext context, HistogramProjector<?> p) { + // register self + final VisualizationTask task = new VisualizationTask(CNAME, context, p, p.getRelation(), ColoredHistogramVisualizer.this); + task.level = VisualizationTask.LEVEL_DATA; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + }); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance + * + * @author Remigius Wojdanowski + * + * @apiviz.has NumberVector oneway - - visualizes + * + * @param <NV> Type of the DatabaseObject being visualized. + */ + // FIXME: cache histogram instead of recomputing it? + public class Instance<NV extends NumberVector> extends AbstractHistogramVisualization { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String BIN = "bin"; + + /** + * The database we visualize + */ + private Relation<NV> relation; + + /** + * Sampling result + */ + private SamplingResult sample; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.relation = task.getRelation(); + this.sample = ResultUtil.getSamplingResult(relation); + addListeners(); + } + + @Override + public void fullRedraw() { + StyleLibrary style = context.getStyleLibrary(); + double margin = style.getSize(StyleLibrary.MARGIN); + layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); + double xsize = Projection.SCALE * getWidth() / getHeight(); + double ysize = Projection.SCALE; + + final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), xsize, ysize, margin); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + // Styling policy + final StylingPolicy spol = context.getStylingPolicy(); + final ClassStylingPolicy cspol; + if(spol instanceof ClassStylingPolicy) { + cspol = (ClassStylingPolicy) spol; + } + else { + cspol = null; + } + // TODO also use min style? + setupCSS(svgp, (cspol != null) ? cspol.getMaxStyle() : 0); + + // Create histograms + final int off = (cspol != null) ? cspol.getMinStyle() : 0; + final int numc = (cspol != null) ? (cspol.getMaxStyle() - cspol.getMinStyle()) : 0; + DoubleMinMax minmax = new DoubleMinMax(); + final double frac = 1. / relation.size(); // TODO: sampling? + final int cols = numc + 1; + DoubleArrayStaticHistogram histogram = new DoubleArrayStaticHistogram(settings.bins, -.5, .5, cols); + + if(cspol != null) { + for(int snum = 0; snum < numc; snum++) { + double[] inc = new double[cols]; + inc[0] = frac; + inc[snum + 1] = frac; + for(DBIDIter iter = cspol.iterateClass(snum + off); iter.valid(); iter.advance()) { + if(!sample.getSample().contains(iter)) { + continue; // TODO: can we test more efficiently than this? + } + try { + double pos = proj.fastProjectDataToRenderSpace(relation.get(iter)) * Projection.INVSCALE; + histogram.increment(pos, inc); + } + catch(ObjectNotFoundException e) { + // Ignore. The object was probably deleted from the database + } + } + } + } + else { + // Actual data distribution. + double[] inc = new double[cols]; + inc[0] = frac; + for(DBIDIter iditer = relation.iterDBIDs(); iditer.valid(); iditer.advance()) { + double pos = proj.fastProjectDataToRenderSpace(relation.get(iditer)) * Projection.INVSCALE; + histogram.increment(pos, inc); + } + } + // for scaling, get the maximum occurring value in the bins: + for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) { + for(double val : iter.getValue()) { + minmax.put(val); + } + } + + LinearScale yscale = new LinearScale(0, minmax.getMax()); + LinearScale xscale = new LinearScale(histogram.getCoverMinimum(), histogram.getCoverMaximum()); + + // Axis. TODO: Add an AxisVisualizer for this? + try { + SVGSimpleLinearAxis.drawAxis(svgp, layer, yscale, 0, ysize, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style); + + // draw axes that are non-trivial + final int dimensionality = RelationUtil.dimensionality(relation); + final double[] vec = new double[dimensionality]; + double orig = proj.fastProjectScaledToRender(vec); + for(int d = 0; d < dimensionality; d++) { + Arrays.fill(vec, 0.); + vec[d] = 1; + // projected endpoint of axis + double ax = proj.fastProjectScaledToRender(vec); + if(ax < orig || ax > orig) { + final double left = (orig / Projection.SCALE + 0.5) * xsize; + final double right = (ax / Projection.SCALE + 0.5) * xsize; + SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getScale(d), left, ysize, right, ysize, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style); + } + } + } + catch(CSSNamingConflict e) { + LoggingUtil.exception("CSS class exception in axis class.", e); + } + + // Visualizing + if(!settings.curves) { + for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) { + double lpos = xscale.getScaled(iter.getLeft()); + double rpos = xscale.getScaled(iter.getRight()); + double stack = 0.0; + final int start = numc > 0 ? 1 : 0; + for(int key = start; key < cols; key++) { + double val = yscale.getScaled(iter.getValue()[key]); + Element row = SVGUtil.svgRect(svgp.getDocument(), xsize * lpos, ysize * (1 - (val + stack)), xsize * (rpos - lpos), ysize * val); + stack = stack + val; + SVGUtil.addCSSClass(row, BIN + (off + key - 1)); + layer.appendChild(row); + } + } + } + else { + double left = xscale.getScaled(histogram.getCoverMinimum()); + double right = left; + + SVGPath[] paths = new SVGPath[cols]; + double[] lasty = new double[cols]; + for(int i = 0; i < cols; i++) { + paths[i] = new SVGPath(xsize * left, ysize * 1); + lasty[i] = 0; + } + + // draw histogram lines + for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) { + left = xscale.getScaled(iter.getLeft()); + right = xscale.getScaled(iter.getRight()); + for(int i = 0; i < cols; i++) { + double val = yscale.getScaled(iter.getValue()[i]); + if(lasty[i] > val || lasty[i] < val) { + paths[i].lineTo(xsize * left, ysize * (1 - lasty[i])); + paths[i].lineTo(xsize * left, ysize * (1 - val)); + paths[i].lineTo(xsize * right, ysize * (1 - val)); + lasty[i] = val; + } + } + } + // close and insert all lines. + for(int i = 0; i < cols; i++) { + if(lasty[i] != 0) { + paths[i].lineTo(xsize * right, ysize * (1 - lasty[i])); + } + paths[i].lineTo(xsize * right, ysize * 1); + Element elem = paths[i].makeElement(svgp); + SVGUtil.addCSSClass(elem, BIN + (off + i - 1)); + layer.appendChild(elem); + } + } + svgp.updateStyleElement(); + } + + /** + * Generate the needed CSS classes. + * + * @param svgp Plot context + * @param numc Number of classes we need. + */ + private void setupCSS(SVGPlot svgp, int numc) { + final StyleLibrary style = context.getStyleLibrary(); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + CSSClass allInOne = new CSSClass(svgp, BIN + -1); + if(!settings.curves) { + allInOne.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + allInOne.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 1.0); + } + else { + allInOne.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + allInOne.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); + allInOne.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + } + svgp.addCSSClassOrLogError(allInOne); + + for(int clusterID = 0; clusterID < numc; clusterID++) { + CSSClass bin = new CSSClass(svgp, BIN + clusterID); + + if(!settings.curves) { + bin.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(clusterID)); + } + else { + bin.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(clusterID)); + bin.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); + bin.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + } + + svgp.addCSSClassOrLogError(bin); + } + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Flag to specify the "curves" rendering style. + * + * <p> + * Key: {@code -histogram.curves} + * </p> + */ + public static final OptionID STYLE_CURVES_ID = new OptionID("projhistogram.curves", "Use curves instead of the stacked histogram style."); + + /** + * Parameter to specify the number of bins to use in histogram. + * + * <p> + * Key: {@code -projhistogram.bins} Default: 80 + * </p> + */ + public static final OptionID HISTOGRAM_BINS_ID = new OptionID("projhistogram.bins", "Number of bins in the distribution histogram"); + + /** + * Internal storage of the curves flag. + */ + protected boolean curves = false; + + /** + * Number of bins to use in the histogram. + */ + protected int bins = DEFAULT_BINS; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + Flag curvesF = new Flag(STYLE_CURVES_ID); + if(config.grab(curvesF)) { + curves = curvesF.isTrue(); + } + IntParameter binsP = new IntParameter(HISTOGRAM_BINS_ID, DEFAULT_BINS); + binsP.addConstraint(CommonConstraints.GREATER_THAN_ONE_INT); + if(config.grab(binsP)) { + bins = binsP.intValue(); + } + } + + @Override + protected ColoredHistogramVisualizer makeInstance() { + return new ColoredHistogramVisualizer(this); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/package-info.java new file mode 100755 index 00000000..d8ed4874 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Visualizers based on 1D projected histograms.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers.histogram;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/AbstractOPTICSVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/AbstractOPTICSVisualization.java new file mode 100644 index 00000000..401e05e6 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/AbstractOPTICSVisualization.java @@ -0,0 +1,94 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.OPTICSProjection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; + +/** + * Abstract base class for OPTICS visualizer + * + * @author Erich Schubert + * + * @apiviz.uses OPTICSProjection + */ +public abstract class AbstractOPTICSVisualization extends AbstractVisualization { + /** + * The plot + */ + final protected OPTICSProjection optics; + + /** + * Width of plot (in display units) + */ + protected double plotwidth; + + /** + * Height of plot (in display units) + */ + protected double plotheight; + + /** + * Constructor. + * + * @param task Visualization task. + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public AbstractOPTICSVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height); + this.optics = (OPTICSProjection) proj; + } + + /** + * Produce a new layer element. + */ + protected void makeLayerElement() { + plotwidth = StyleLibrary.SCALE; + plotheight = StyleLibrary.SCALE / optics.getOPTICSPlot(context).getRatio(); + final double margin = context.getStyleLibrary().getSize(StyleLibrary.MARGIN); + layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); + final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), plotwidth, plotheight, margin * .5, margin * .5, margin * 1.5, margin * .5); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + } + + /** + * Access the raw cluster order + * + * @return Cluster order + */ + protected ClusterOrder getClusterOrder() { + return optics.getResult(); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSClusterVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSClusterVisualization.java new file mode 100644 index 00000000..e1cd5c66 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSClusterVisualization.java @@ -0,0 +1,233 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.HashMap; +import java.util.Map; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.model.OPTICSModel; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualize the clusters and cluster hierarchy found by OPTICS on the OPTICS + * Plot. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class OPTICSClusterVisualization extends AbstractVisFactory { + /** + * The logger for this class. + */ + private static final Logging LOG = Logging.getLogger(OPTICSClusterVisualization.class); + + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "OPTICS Cluster Ranges"; + + /** + * Constructor. + */ + public OPTICSClusterVisualization() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class); + for(; it.valid(); it.advance()) { + OPTICSProjector p = it.get(); + final Clustering<OPTICSModel> ocl = findOPTICSClustering(context, p.getResult()); + if(ocl != null) { + final VisualizationTask task = new VisualizationTask(NAME, context, ocl, null, this); + task.level = VisualizationTask.LEVEL_DATA; + context.addVis(p, task); + // TODO: use and react to style policy! + } + } + // TODO: also run when a new clustering is added, instead of just new + // projections? + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Find the first OPTICS clustering child of a result. + * + * @param context Result context + * @param start Result to start searching at + * @return OPTICS clustering + */ + @SuppressWarnings("unchecked") + protected static Clustering<OPTICSModel> findOPTICSClustering(VisualizerContext context, Result start) { + Hierarchy.Iter<Clustering<?>> it1 = VisualizationTree.filterResults(context, start, Clustering.class); + for(; it1.valid(); it1.advance()) { + Clustering<?> clus = it1.get(); + if(clus.getToplevelClusters().size() == 0) { + continue; + } + try { + Cluster<?> firstcluster = clus.getToplevelClusters().iterator().next(); + if(firstcluster.getModel() instanceof OPTICSModel) { + return (Clustering<OPTICSModel>) clus; + } + } + catch(Exception e) { + // Empty clustering? Shouldn't happen. + LOG.warning("Clustering with no cluster detected.", e); + } + } + return null; + } + + /** + * Instance. + * + * @author Erich Schubert + * + * @apiviz.uses Clustering oneway - - «visualizes» + */ + public class Instance extends AbstractOPTICSVisualization { + /** + * CSS class for markers + */ + protected static final String CSS_BRACKET = "opticsBracket"; + + /** + * Our clustering + */ + Clustering<OPTICSModel> clus; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.clus = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + makeLayerElement(); + addCSSClasses(); + + ColorLibrary colors = context.getStyleLibrary().getColorSet(StyleLibrary.PLOT); + HashMap<Cluster<?>, String> colormap = new HashMap<>(); + int cnum = 0; + for(Cluster<?> c : clus.getAllClusters()) { + colormap.put(c, colors.getColor(cnum)); + cnum++; + } + drawClusters(clus, clus.iterToplevelClusters(), 1, colormap); + } + + /** + * Recursively draw clusters + * + * @param clusters Current set of clusters + * @param depth Recursion depth + * @param colormap Color mapping + */ + private void drawClusters(Clustering<OPTICSModel> clustering, Hierarchy.Iter<Cluster<OPTICSModel>> clusters, int depth, Map<Cluster<?>, String> colormap) { + final double scale = StyleLibrary.SCALE; + + for(; clusters.valid(); clusters.advance()) { + Cluster<OPTICSModel> cluster = clusters.get(); + try { + OPTICSModel model = cluster.getModel(); + final double x1 = plotwidth * ((model.getStartIndex() + .25) / this.optics.getResult().size()); + final double x2 = plotwidth * ((model.getEndIndex() + .75) / this.optics.getResult().size()); + final double y = plotheight + depth * scale * 0.01; + Element e = svgp.svgLine(x1, y, x2, y); + SVGUtil.addCSSClass(e, CSS_BRACKET); + String color = colormap.get(cluster); + if(color != null) { + SVGUtil.setAtt(e, SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_STROKE_PROPERTY + ":" + color); + } + layer.appendChild(e); + } + catch(ClassCastException e) { + LOG.warning("Expected OPTICSModel, got: " + cluster.getModel().getClass().getSimpleName()); + } + // Descend + final Hierarchy.Iter<Cluster<OPTICSModel>> children = clustering.getClusterHierarchy().iterChildren(cluster); + if(children != null) { + drawClusters(clustering, children, depth + 1, colormap); + } + } + } + + /** + * Adds the required CSS-Classes + */ + private void addCSSClasses() { + // Class for the markers + if(!svgp.getCSSClassManager().contains(CSS_BRACKET)) { + final CSSClass cls = new CSSClass(this, CSS_BRACKET); + final StyleLibrary style = context.getStyleLibrary(); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); + svgp.addCSSClassOrLogError(cls); + } + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotCutVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotCutVisualization.java new file mode 100644 index 00000000..65e00430 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotCutVisualization.java @@ -0,0 +1,298 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVG12Constants; +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.utilities.FormatUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSCut; +import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualizes a cut in an OPTICS Plot to select an Epsilon value and generate a + * new clustering result. + * + * @author Heidi Kolb + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class OPTICSPlotCutVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "OPTICS Cut"; + + /** + * Constructor. + */ + public OPTICSPlotCutVisualization() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class); + for(; it.valid(); it.advance()) { + OPTICSProjector p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + context.addVis(p, task); + } + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance. + * + * @author Heidi Kolb + * @author Erich Schubert + */ + public class Instance extends AbstractOPTICSVisualization implements DragableArea.DragListener { + /** + * CSS-Styles + */ + protected static final String CSS_LINE = "opticsPlotLine"; + + /** + * CSS-Styles + */ + protected static final String CSS_EPSILON = "opticsPlotEpsilonValue"; + + /** + * The current epsilon value. + */ + private double epsilon = 0.0; + + /** + * Sensitive (clickable) area + */ + private DragableArea eventarea = null; + + /** + * The label element + */ + private Element elemText = null; + + /** + * The line element + */ + private Element elementLine = null; + + /** + * The drag handle element + */ + private Element elementPoint = null; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + } + + @Override + public void fullRedraw() { + incrementalRedraw(); + } + + @Override + public void incrementalRedraw() { + if(layer == null) { + makeLayerElement(); + addCSSClasses(); + } + + // TODO make the number of digits configurable + final String label = (epsilon > 0.0) ? FormatUtil.NF4.format(epsilon) : ""; + // compute absolute y-value of bar + final double yAct = getYFromEpsilon(epsilon); + + if(elemText == null) { + elemText = svgp.svgText(StyleLibrary.SCALE * 1.05, yAct, label); + SVGUtil.setAtt(elemText, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_EPSILON); + layer.appendChild(elemText); + } + else { + elemText.setTextContent(label); + SVGUtil.setAtt(elemText, SVGConstants.SVG_Y_ATTRIBUTE, yAct); + } + + // line and handle + if(elementLine == null) { + elementLine = svgp.svgLine(0, yAct, StyleLibrary.SCALE * 1.04, yAct); + SVGUtil.addCSSClass(elementLine, CSS_LINE); + layer.appendChild(elementLine); + } + else { + SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y1_ATTRIBUTE, yAct); + SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y2_ATTRIBUTE, yAct); + } + if(elementPoint == null) { + elementPoint = svgp.svgCircle(StyleLibrary.SCALE * 1.04, yAct, StyleLibrary.SCALE * 0.004); + SVGUtil.addCSSClass(elementPoint, CSS_LINE); + layer.appendChild(elementPoint); + } + else { + SVGUtil.setAtt(elementPoint, SVG12Constants.SVG_CY_ATTRIBUTE, yAct); + } + + if(eventarea == null) { + eventarea = new DragableArea(svgp, StyleLibrary.SCALE, -StyleLibrary.SCALE * 0.01, // + StyleLibrary.SCALE * 0.1, plotheight + StyleLibrary.SCALE * 0.02, this); + layer.appendChild(eventarea.getElement()); + } + } + + @Override + public void destroy() { + super.destroy(); + eventarea.destroy(); + } + + /** + * Get epsilon from y-value + * + * @param y y-Value + * @return epsilon + */ + protected double getEpsilonFromY(double y) { + OPTICSPlot opticsplot = optics.getOPTICSPlot(context); + y = (y < 0) ? 0 : (y > plotheight) ? 1. : y / plotheight; + return optics.getOPTICSPlot(context).scaleFromPixel(y * opticsplot.getHeight()); + } + + /** + * Get y-value from epsilon + * + * @param epsilon epsilon + * @return y-Value + */ + protected double getYFromEpsilon(double epsilon) { + OPTICSPlot opticsplot = optics.getOPTICSPlot(context); + int h = opticsplot.getHeight(); + double y = opticsplot.getScale().getScaled(epsilon, h - .5, .5) / (double) h * plotheight; + return (y < 0.) ? 0. : (y > plotheight) ? plotheight : y; + } + + @Override + public boolean startDrag(SVGPoint start, Event evt) { + epsilon = getEpsilonFromY(plotheight - start.getY()); + // opvis.unsetEpsilonExcept(this); + svgp.requestRedraw(this.task, this); + return true; + } + + @Override + public boolean duringDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) { + if(inside) { + epsilon = getEpsilonFromY(plotheight - end.getY()); + } + // opvis.unsetEpsilonExcept(this); + svgp.requestRedraw(this.task, this); + return true; + } + + @Override + public boolean endDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) { + if(inside) { + epsilon = getEpsilonFromY(plotheight - end.getY()); + // opvis.unsetEpsilonExcept(this); + + // FIXME: replace an existing optics cut result! + final ClusterOrder order = optics.getResult(); + Clustering<Model> cl = OPTICSCut.makeOPTICSCut(order, epsilon); + order.addChildResult(cl); + } + svgp.requestRedraw(this.task, this); + return true; + } + + /** + * Reset the epsilon value. + */ + public void unsetEpsilon() { + epsilon = 0.0; + } + + /** + * Adds the required CSS-Classes + */ + private void addCSSClasses() { + // Class for the epsilon-value + final StyleLibrary style = context.getStyleLibrary(); + if(!svgp.getCSSClassManager().contains(CSS_EPSILON)) { + final CSSClass label = new CSSClass(svgp, CSS_EPSILON); + label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL)); + label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL)); + label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL)); + svgp.addCSSClassOrLogError(label); + } + // Class for the epsilon cut line + if(!svgp.getCSSClassManager().contains(CSS_LINE)) { + final CSSClass lcls = new CSSClass(svgp, CSS_LINE); + lcls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.PLOT)); + lcls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, 0.5 * style.getLineWidth(StyleLibrary.PLOT)); + svgp.addCSSClassOrLogError(lcls); + } + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotSelectionVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotSelectionVisualization.java new file mode 100644 index 00000000..7abc5609 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotSelectionVisualization.java @@ -0,0 +1,360 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.dom.events.DOMMouseEvent; +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.ids.HashSetModifiableDBIDs; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Handle the marker in an OPTICS plot. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class OPTICSPlotSelectionVisualization extends AbstractVisFactory { + /** + * The logger for this class. + */ + private static final Logging LOG = Logging.getLogger(OPTICSPlotSelectionVisualization.class); + + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "OPTICS Selection"; + + /** + * Input modes + * + * @apiviz.exclude + */ + // TODO: Refactor all Mode copies into a shared class? + private enum Mode { + REPLACE, ADD, INVERT + } + + /** + * Constructor. + */ + public OPTICSPlotSelectionVisualization() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class); + for(; it.valid(); it.advance()) { + OPTICSProjector p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.addUpdateFlags(VisualizationTask.ON_SELECTION); + context.addVis(p, task); + } + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance. + * + * @author Heidi Kolb + * @author Erich Schubert + * + * @apiviz.uses DBIDSelection oneway - 1 visualizes + */ + public class Instance extends AbstractOPTICSVisualization implements DragableArea.DragListener { + /** + * CSS class for markers + */ + protected static final String CSS_MARKER = "opticsPlotMarker"; + + /** + * CSS class for markers + */ + protected static final String CSS_RANGEMARKER = "opticsPlotRangeMarker"; + + /** + * Element for the events + */ + private Element etag; + + /** + * Element for the marker + */ + private Element mtag; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + makeLayerElement(); + addCSSClasses(); + + mtag = svgp.svgElement(SVGConstants.SVG_G_TAG); + addMarker(); + + DragableArea drag = new DragableArea(svgp, 0 - plotwidth * 0.1, 0, plotwidth * 1.1, plotheight, this); + etag = drag.getElement(); + // mtag first, etag must be the top Element + layer.appendChild(mtag); + layer.appendChild(etag); + } + + /** + * Add marker for the selected IDs to mtag + */ + public void addMarker() { + ClusterOrder order = getClusterOrder(); + // TODO: replace mtag! + DBIDSelection selContext = context.getSelection(); + if(selContext != null) { + DBIDs selection = DBIDUtil.ensureSet(selContext.getSelectedIds()); + + final double width = plotwidth / order.size(); + int begin = -1, j = 0; + for(DBIDIter it = order.iter(); it.valid(); it.advance(), j++) { + if(selection.contains(it)) { + if(begin == -1) { + begin = j; + } + } + else { + if(begin != -1) { + Element marker = addMarkerRect(begin * width, (j - begin) * width); + SVGUtil.addCSSClass(marker, CSS_MARKER); + mtag.appendChild(marker); + begin = -1; + } + } + } + // tail + if(begin != -1) { + Element marker = addMarkerRect(begin * width, (order.size() - begin) * width); + SVGUtil.addCSSClass(marker, CSS_MARKER); + mtag.appendChild(marker); + } + } + } + + /** + * Create a rectangle as marker (Marker higher than plot!) + * + * @param x1 X-Value for the marker + * @param width Width of an entry + * @return SVG-Element svg-rectangle + */ + public Element addMarkerRect(double x1, double width) { + return svgp.svgRect(x1, 0, width, plotheight); + } + + @Override + public boolean startDrag(SVGPoint startPoint, Event evt) { + ClusterOrder order = getClusterOrder(); + int mouseActIndex = getSelectedIndex(order, startPoint); + if(mouseActIndex >= 0 && mouseActIndex < order.size()) { + double width = plotwidth / order.size(); + double x1 = mouseActIndex * width; + Element marker = addMarkerRect(x1, width); + SVGUtil.setCSSClass(marker, CSS_RANGEMARKER); + mtag.appendChild(marker); + return true; + } + return false; + } + + @Override + public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + ClusterOrder order = getClusterOrder(); + int mouseDownIndex = getSelectedIndex(order, startPoint); + int mouseActIndex = getSelectedIndex(order, dragPoint); + final int begin = Math.max(Math.min(mouseDownIndex, mouseActIndex), 0); + final int end = Math.min(Math.max(mouseDownIndex, mouseActIndex), order.size()); + double width = plotwidth / order.size(); + double x1 = begin * width; + double x2 = (end * width) + width; + mtag.removeChild(mtag.getLastChild()); + Element marker = addMarkerRect(x1, x2 - x1); + SVGUtil.setCSSClass(marker, CSS_RANGEMARKER); + mtag.appendChild(marker); + return true; + } + + @Override + public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + ClusterOrder order = getClusterOrder(); + int mouseDownIndex = getSelectedIndex(order, startPoint); + int mouseActIndex = getSelectedIndex(order, dragPoint); + Mode mode = getInputMode(evt); + final int begin = Math.max(Math.min(mouseDownIndex, mouseActIndex), 0); + final int end = Math.min(Math.max(mouseDownIndex, mouseActIndex), order.size()); + updateSelection(mode, begin, end); + return true; + } + + /** + * Get the current input mode, on each mouse event. + * + * @param evt Mouse event. + * @return Input mode + */ + private Mode getInputMode(Event evt) { + if(evt instanceof DOMMouseEvent) { + DOMMouseEvent domme = (DOMMouseEvent) evt; + // TODO: visual indication of mode possible? + if(domme.getShiftKey()) { + return Mode.ADD; + } + else if(domme.getCtrlKey()) { + return Mode.INVERT; + } + else { + return Mode.REPLACE; + } + } + // Default mode is replace. + return Mode.REPLACE; + } + + /** + * Gets the Index of the ClusterOrderEntry where the event occurred + * + * @param order List of ClusterOrderEntries + * @param cPt clicked point + * @return Index of the object + */ + private int getSelectedIndex(ClusterOrder order, SVGPoint cPt) { + int mouseActIndex = (int) ((cPt.getX() / plotwidth) * order.size()); + return mouseActIndex; + } + + /** + * Updates the selection for the given ClusterOrderEntry. + * + * @param mode Input mode + * @param begin first index to select + * @param end last index to select + */ + protected void updateSelection(Mode mode, int begin, int end) { + ClusterOrder order = getClusterOrder(); + if(begin < 0 || begin > end || end >= order.size()) { + LOG.warning("Invalid range in updateSelection: " + begin + " .. " + end); + return; + } + + DBIDSelection selContext = context.getSelection(); + HashSetModifiableDBIDs selection; + if(selContext == null || mode == Mode.REPLACE) { + selection = DBIDUtil.newHashSet(); + } + else { + selection = DBIDUtil.newHashSet(selContext.getSelectedIds()); + } + + for(DBIDArrayIter it = order.iter().seek(begin); it.getOffset() <= end; it.advance()) { + if(mode == Mode.INVERT) { + if(!selection.contains(it)) { + selection.add(it); + } + else { + selection.remove(it); + } + } + else { + // In REPLACE and ADD, add objects. + // The difference was done before by not re-using the selection. + // Since we are using a set, we can just add in any case. + selection.add(it); + } + } + context.setSelection(new DBIDSelection(selection)); + } + + /** + * Adds the required CSS-Classes + */ + private void addCSSClasses() { + // Class for the markers + if(!svgp.getCSSClassManager().contains(CSS_MARKER)) { + final CSSClass cls = new CSSClass(this, CSS_MARKER); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLUE_VALUE); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.2"); + svgp.addCSSClassOrLogError(cls); + } + + // Class for the range marking + if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) { + final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER); + rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE); + rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.2"); + svgp.addCSSClassOrLogError(rcls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotVisualizer.java new file mode 100644 index 00000000..65d8ecf3 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotVisualizer.java @@ -0,0 +1,143 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualize an OPTICS result by constructing an OPTICS plot for it. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class OPTICSPlotVisualizer extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "OPTICS Plot"; + + /** + * Constructor. + */ + public OPTICSPlotVisualizer() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class); + for(; it.valid(); it.advance()) { + OPTICSProjector p = it.get(); + // Add plots, attach visualizer + final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this); + task.level = VisualizationTask.LEVEL_DATA; + // FIXME: task.setUpdates(VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance. + * + * @author Erich Schubert + */ + public class Instance extends AbstractOPTICSVisualization { + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + makeLayerElement(); + // addCSSClasses(); + + OPTICSPlot opticsplot = optics.getOPTICSPlot(context); + String ploturi = opticsplot.getSVGPlotURI(); + + Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG); + SVGUtil.setAtt(itag, SVGConstants.SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE); + SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE); + SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, 0); + SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, 0); + SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, plotwidth); + SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, plotheight); + itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, ploturi); + + layer.appendChild(itag); + + LinearScale scale = opticsplot.getScale(); + double y1 = plotheight * opticsplot.scaleToPixel(scale.getMin()) / opticsplot.getHeight(); + double y2 = plotheight * opticsplot.scaleToPixel(scale.getMax()) / opticsplot.getHeight(); + try { + final StyleLibrary style = context.getStyleLibrary(); + SVGSimpleLinearAxis.drawAxis(svgp, layer, scale, 0, y1, 0, y2, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style); + SVGSimpleLinearAxis.drawAxis(svgp, layer, scale, plotwidth, y1, plotwidth, y2, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style); + } + catch(CSSNamingConflict e) { + LoggingUtil.exception("CSS naming conflict for axes on OPTICS plot", e); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSSteepAreaVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSSteepAreaVisualization.java new file mode 100644 index 00000000..ef8f6198 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSSteepAreaVisualization.java @@ -0,0 +1,211 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.Color; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.OPTICSXi; +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.OPTICSXi.SteepAreaResult; +import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualize the steep areas found in an OPTICS plot + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class OPTICSSteepAreaVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "OPTICS Steep Areas"; + + /** + * Constructor. + */ + public OPTICSSteepAreaVisualization() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class); + for(; it.valid(); it.advance()) { + OPTICSProjector p = it.get(); + final SteepAreaResult steep = findSteepAreaResult(p.getResult()); + if(steep != null) { + final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this); + task.level = VisualizationTask.LEVEL_DATA + 1; + context.addVis(p, task); + context.addVis(steep, task); + } + } + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Find the OPTICS clustering child of a cluster order. + * + * @param co Cluster order + * @return OPTICS clustering + */ + protected static OPTICSXi.SteepAreaResult findSteepAreaResult(ClusterOrder co) { + for(Hierarchy.Iter<Result> r = co.getHierarchy().iterChildren(co); r.valid(); r.advance()) { + if(OPTICSXi.SteepAreaResult.class.isInstance(r.get())) { + return (OPTICSXi.SteepAreaResult) r.get(); + } + } + return null; + } + + /** + * Instance + * + * @author Erich Schubert + * + * @apiviz.uses SteepAreaResult + */ + public class Instance extends AbstractOPTICSVisualization { + /** + * CSS class for markers + */ + protected static final String CSS_STEEP_UP = "opticsSteepUp"; + + /** + * CSS class for markers + */ + protected static final String CSS_STEEP_DOWN = "opticsSteepDown"; + + /** + * Our clustering + */ + OPTICSXi.SteepAreaResult areas; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.areas = findSteepAreaResult(this.optics.getResult()); + } + + @Override + public void fullRedraw() { + makeLayerElement(); + addCSSClasses(); + + final OPTICSPlot opticsplot = optics.getOPTICSPlot(context); + final ClusterOrder co = getClusterOrder(); + final int oheight = opticsplot.getHeight(); + final double xscale = plotwidth / (double) co.size(); + final double yscale = plotheight / (double) oheight; + + DBIDArrayIter tmp = co.iter(); + + for(OPTICSXi.SteepArea area : areas) { + final int st = area.getStartIndex(); + final int en = area.getEndIndex(); + // Note: make sure we are using doubles! + final double x1 = (st + .25); + final double x2 = (en + .75); + final double y1 = opticsplot.scaleToPixel(co.getReachability(tmp.seek(st))); + final double y2 = opticsplot.scaleToPixel(co.getReachability(tmp.seek(en))); + Element e = svgp.svgLine(x1 * xscale, y1 * yscale, x2 * xscale, y2 * yscale); + if(area instanceof OPTICSXi.SteepDownArea) { + SVGUtil.addCSSClass(e, CSS_STEEP_DOWN); + } + else { + SVGUtil.addCSSClass(e, CSS_STEEP_UP); + } + layer.appendChild(e); + } + } + + /** + * Adds the required CSS-Classes + */ + private void addCSSClasses() { + // Class for the markers + final StyleLibrary style = context.getStyleLibrary(); + if(!svgp.getCSSClassManager().contains(CSS_STEEP_DOWN)) { + final CSSClass cls = new CSSClass(this, CSS_STEEP_DOWN); + Color color = SVGUtil.stringToColor(style.getColor(StyleLibrary.PLOT)); + if(color == null) { + color = Color.BLACK; + } + color = new Color((int) (color.getRed() * 0.6), (int) (color.getGreen() * 0.6 + 0.4 * 256.), (int) (color.getBlue() * 0.6)); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGUtil.colorToString(color)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(CSS_STEEP_UP)) { + final CSSClass cls = new CSSClass(this, CSS_STEEP_UP); + Color color = SVGUtil.stringToColor(style.getColor(StyleLibrary.PLOT)); + if(color == null) { + color = Color.BLACK; + } + color = new Color((int) (color.getRed() * 0.6 + 0.4 * 256.), (int) (color.getGreen() * 0.6), (int) (color.getBlue() * 0.6)); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGUtil.colorToString(color)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5); + svgp.addCSSClassOrLogError(cls); + } + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/package-info.java new file mode 100755 index 00000000..3f6f7e60 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers that do work on OPTICS plots</p> + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers.optics;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/package-info.java new file mode 100755 index 00000000..193a5f2b --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/package-info.java @@ -0,0 +1,30 @@ +/** + * <p>Visualizers for various results</p> + * + * @apiviz.exclude de.lmu.ifi.dbs.elki.utilities.datastructures.AnyMap + * @apiviz.exclude Visualization.Factory + * @apiviz.exclude de.lmu.ifi.dbs.elki.visualization.gui.* + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers; diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/CircleSegmentsVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/CircleSegmentsVisualizer.java new file mode 100644 index 00000000..72e9ccef --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/CircleSegmentsVisualizer.java @@ -0,0 +1,746 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.Color; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; +import org.w3c.dom.events.MouseEvent; + +import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segment; +import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segments; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultListener; +import de.lmu.ifi.dbs.elki.utilities.FormatUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.documentation.Reference; +import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGCheckbox; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualizer to draw circle segments of clusterings and enable interactive + * selection of segments. For "empty" segments, all related segments are + * selected instead, to visualize the differences. + * + * <p> + * Reference:<br /> + * Evaluation of Clusterings – Metrics and Visual Support<br /> + * Elke Achtert, Sascha Goldhofer, Hans-Peter Kriegel, Erich Schubert, Arthur + * Zimek<br /> + * In: Proc. 28th International Conference on Data Engineering (ICDE) 2012 + * </p> + * + * <p> + * Details on the experimental setup can be found at: + * <a href="http://elki.dbs.ifi.lmu.de/wiki/Examples/ClusterEvaluation" >wiki/ + * Examples/ClusterEvaluation</a> + * </p> + * + * @author Sascha Goldhofer + * @author Erich Schubert + * + * @apiviz.landmark + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +@Reference(title = "Evaluation of Clusterings – Metrics and Visual Support", // +authors = "Elke Achtert, Sascha Goldhofer, Hans-Peter Kriegel, Erich Schubert, Arthur Zimek", // +booktitle = "Proc. 28th International Conference on Data Engineering (ICDE) 2012", // +url = "http://dx.doi.org/10.1109/ICDE.2012.128") +public class CircleSegmentsVisualizer extends AbstractVisFactory { + /** + * Class logger + */ + private static final Logging LOG = Logging.getLogger(CircleSegmentsVisualizer.class); + + /** + * CircleSegments visualizer name + */ + private static final String NAME = "CircleSegments"; + + /** + * Constructor + */ + public CircleSegmentsVisualizer() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<Segments> it1 = VisualizationTree.filterResults(context, start, Segments.class); + for(; it1.valid(); it1.advance()) { + Segments segmentResult = it1.get(); + SegmentsStylingPolicy policy; + Hierarchy.Iter<SegmentsStylingPolicy> it = VisualizationTree.filter(context, segmentResult, SegmentsStylingPolicy.class); + if(it.valid()) { + policy = it.get(); + } + else { + policy = new SegmentsStylingPolicy(segmentResult); + context.addVis(segmentResult, policy); + } + // create task for visualization + final VisualizationTask task = new VisualizationTask(NAME, context, policy, null, this); + task.reqwidth = 2.0; + task.reqheight = 2.0; + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY); + context.addVis(segmentResult, task); + } + } + + /** + * Instance + * + * @author Sascha Goldhofer + * @author Erich Schubert + * + * @apiviz.uses Segments + * @apiviz.has SegmentsStylingPolicy + * + */ + public class Instance extends AbstractVisualization implements ResultListener { + /** Minimum width (radian) of Segment */ + private static final double SEGMENT_MIN_ANGLE = 0.01; + + /** Gap (radian) between segments */ + private static final double SEGMENT_MIN_SEP_ANGLE = 0.005; + + /** Offset from center to first ring */ + private static final double RADIUS_INNER = 0.04 * StyleLibrary.SCALE; + + /** Margin between two rings */ + private static final double RADIUS_DISTANCE = 0.01 * StyleLibrary.SCALE; + + /** Radius of whole CircleSegments except selection border */ + private static final double RADIUS_OUTER = 0.47 * StyleLibrary.SCALE; + + /** Radius of highlight selection (outer ring) */ + private static final double RADIUS_SELECTION = 0.02 * StyleLibrary.SCALE; + + /** + * CSS class name for the clusterings. + */ + private static final String CLR_CLUSTER_CLASS_PREFIX = "clusterSegment"; + + /** + * CSS border class of a cluster + */ + public static final String CLR_BORDER_CLASS = "clusterBorder"; + + /** + * CSS hover class for clusters of hovered segment + */ + public static final String CLR_UNPAIRED_CLASS = "clusterUnpaired"; + + /** + * CSS hover class of a segment cluster + */ + public static final String CLR_HOVER_CLASS = "clusterHover"; + + /** + * CSS class of selected Segment + */ + public static final String SEG_UNPAIRED_SELECTED_CLASS = "unpairedSegmentSelected"; + + /** + * Style prefix + */ + public static final String STYLE = "segments"; + + /** + * Style for border lines + */ + public static final String STYLE_BORDER = STYLE + ".border"; + + /** + * Style for hover effect + */ + public static final String STYLE_HOVER = STYLE + ".hover"; + + /** + * First color for producing segment-cluster colors + */ + public static final String STYLE_GRADIENT_FIRST = STYLE + ".cluster.first"; + + /** + * Second color for producing segment-cluster colors + */ + public static final String STYLE_GRADIENT_SECOND = STYLE + ".cluster.second"; + + /** + * Segmentation of Clusterings + */ + protected final Segments segments; + + /** + * The two main layers + */ + private Element visLayer, ctrlLayer; + + /** + * Map to connect segments to their visual elements + */ + public Map<Segment, List<Element>> segmentToElements = new HashMap<>(); + + /** + * Show unclustered Pairs in CircleSegments + */ + boolean showUnclusteredPairs = false; + + /** + * Styling policy + */ + protected final SegmentsStylingPolicy policy; + + /** + * Flag to disallow an incremental redraw + */ + private boolean noIncrementalRedraw = true; + + /** + * Constructor + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height); + policy = task.getResult(); + segments = policy.segments; + // FIXME: handle this more generally. + policy.setStyleLibrary(context.getStyleLibrary()); + addListeners(); + } + + public void toggleUnclusteredPairs(boolean show) { + noIncrementalRedraw = true; + showUnclusteredPairs = show; + svgp.requestRedraw(this.task, this); + } + + @Override + public void resultChanged(Result current) { + super.resultChanged(current); + if(current == context.getStylingPolicy()) { + // When switching to a different policy, unhighlight segments. + if(context.getStylingPolicy() != policy) { + policy.deselectAllSegments(); + } + } + } + + @Override + public void incrementalRedraw() { + if(noIncrementalRedraw) { + super.incrementalRedraw(); + } + else { + redrawSelection(); + } + } + + @Override + public void fullRedraw() { + LOG.debug("Full redraw"); + noIncrementalRedraw = false; // Done that. + + // initialize css (needs clusterSize!) + addCSSClasses(segments.getHighestClusterCount()); + + layer = svgp.svgElement(SVGConstants.SVG_G_TAG); + visLayer = svgp.svgElement(SVGConstants.SVG_G_TAG); + // Setup scaling for canvas: 0 to StyleLibrary.SCALE (usually 100 to avoid + // a + // Java drawing bug!) + String transform = SVGUtil.makeMarginTransform(task.reqwidth, task.reqheight, StyleLibrary.SCALE, StyleLibrary.SCALE, 0) + " translate(" + (StyleLibrary.SCALE * .5) + " " + (StyleLibrary.SCALE * .5) + ")"; + visLayer.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + ctrlLayer = svgp.svgElement(SVGConstants.SVG_G_TAG); + + // and create svg elements + drawSegments(); + + // + // Build Interface + // + SVGCheckbox checkbox = new SVGCheckbox(showUnclusteredPairs, "Show unclustered pairs"); + checkbox.addCheckBoxListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + toggleUnclusteredPairs(((SVGCheckbox) e.getSource()).isChecked()); + } + }); + + // Add ring:clustering info + Element clrInfo = drawClusteringInfo(); + Element c = checkbox.renderCheckBox(svgp, 1., 5. + FormatUtil.parseDouble(clrInfo.getAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE)), 11); + ctrlLayer.appendChild(clrInfo); + ctrlLayer.appendChild(c); + + ctrlLayer.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "scale(" + (0.25 / StyleLibrary.SCALE) + ")"); + + layer.appendChild(visLayer); + layer.appendChild(ctrlLayer); + } + + /** + * Define and add required CSS classes + */ + protected void addCSSClasses(int maxClusterSize) { + StyleLibrary style = context.getStyleLibrary(); + + // Cluster separation lines + CSSClass cssReferenceBorder = new CSSClass(this.getClass(), CLR_BORDER_CLASS); + cssReferenceBorder.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getColor(STYLE_BORDER)); + svgp.addCSSClassOrLogError(cssReferenceBorder); + + // Hover effect for clusters + CSSClass cluster_hover = new CSSClass(this.getClass(), CLR_HOVER_CLASS); + // Note: !important is needed to override the regular color assignment + cluster_hover.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getColor(STYLE_HOVER) + " !important"); + cluster_hover.setStatement(SVGConstants.SVG_CURSOR_TAG, SVGConstants.SVG_POINTER_VALUE); + svgp.addCSSClassOrLogError(cluster_hover); + + // Unpaired cluster segment + CSSClass cluster_unpaired = new CSSClass(this.getClass(), CLR_UNPAIRED_CLASS); + cluster_unpaired.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getBackgroundColor(STYLE)); + cluster_unpaired.setStatement(SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(cluster_unpaired); + + // Selected unpaired cluster segment + CSSClass cluster_unpaired_s = new CSSClass(this.getClass(), SEG_UNPAIRED_SELECTED_CLASS); + cluster_unpaired_s.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getColor(STYLE_HOVER) + " !important"); + svgp.addCSSClassOrLogError(cluster_unpaired_s); + + // create Color shades for clusters + String firstcol = style.getColor(STYLE_GRADIENT_FIRST); + String secondcol = style.getColor(STYLE_GRADIENT_SECOND); + String[] clusterColorShades = makeGradient(maxClusterSize, new String[] { firstcol, secondcol }); + + for(int i = 0; i < maxClusterSize; i++) { + CSSClass clusterClasses = new CSSClass(CircleSegmentsVisualizer.class, CLR_CLUSTER_CLASS_PREFIX + "_" + i); + clusterClasses.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, clusterColorShades[i]); + clusterClasses.setStatement(SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE); + svgp.addCSSClassOrLogError(clusterClasses); + } + } + + /** + * Create the segments + */ + private void drawSegments() { + final StyleLibrary style = context.getStyleLibrary(); + final int clusterings = segments.getClusterings(); + + // Reinitialize + this.segmentToElements.clear(); + + double angle_pair = (MathUtil.TWOPI - (SEGMENT_MIN_SEP_ANGLE * segments.size())) / segments.getPairCount(showUnclusteredPairs); + final int pair_min_count = (int) Math.ceil(SEGMENT_MIN_ANGLE / angle_pair); + + // number of segments needed to be resized + int cluster_min_count = 0; + for(Segment segment : segments) { + if(segment.getPairCount() <= pair_min_count) { + cluster_min_count++; + } + } + + // update width of a pair + angle_pair = (MathUtil.TWOPI - (SEGMENT_MIN_SEP_ANGLE * segments.size() + cluster_min_count * SEGMENT_MIN_ANGLE)) / (segments.getPairCount(showUnclusteredPairs) - cluster_min_count); + double radius_delta = (RADIUS_OUTER - RADIUS_INNER - clusterings * RADIUS_DISTANCE) / clusterings; + double border_width = SEGMENT_MIN_SEP_ANGLE; + + int refClustering = 0; + int refSegment = Segment.UNCLUSTERED; + double offsetAngle = 0.0; + + for(final Segment segment : segments) { + long currentPairCount = segment.getPairCount(); + + // resize small segments if below minimum + double alpha = SEGMENT_MIN_ANGLE; + if(currentPairCount > pair_min_count) { + alpha = angle_pair * currentPairCount; + } + + // ITERATE OVER ALL SEGMENT-CLUSTERS + + ArrayList<Element> elems = new ArrayList<>(clusterings); + segmentToElements.put(segment, elems); + // draw segment for every clustering + + for(int i = 0; i < clusterings; i++) { + double currentRadius = i * (radius_delta + RADIUS_DISTANCE) + RADIUS_INNER; + + // Add border if the next segment is a different cluster in the + // reference clustering + if((refSegment != segment.get(refClustering)) && refClustering == i) { + Element border = SVGUtil.svgCircleSegment(svgp, 0, 0, offsetAngle - SEGMENT_MIN_SEP_ANGLE, border_width, currentRadius, RADIUS_OUTER - RADIUS_DISTANCE); + border.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_BORDER_CLASS); + visLayer.appendChild(border); + + if(segment.get(refClustering) == Segment.UNCLUSTERED) { + refClustering = Math.min(refClustering + 1, clusterings - 1); + } + refSegment = segment.get(refClustering); + } + + int cluster = segment.get(i); + + // create ring segment + Element segelement = SVGUtil.svgCircleSegment(svgp, 0, 0, offsetAngle, alpha, currentRadius, currentRadius + radius_delta); + elems.add(segelement); + + // MouseEvents on segment cluster + EventListener listener = new SegmentListenerProxy(segment, i); + EventTarget targ = (EventTarget) segelement; + targ.addEventListener(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE, listener, false); + targ.addEventListener(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE, listener, false); + targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, listener, false); + + // Coloring based on clusterID + if(cluster >= 0) { + segelement.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_CLUSTER_CLASS_PREFIX + "_" + cluster); + } + // if its an unpaired cluster set color to white + else { + segelement.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_UNPAIRED_CLASS); + } + + visLayer.appendChild(segelement); + } + + // + // Add a extended strip for each segment to emphasis selection + // (easier to track thin segments and their color coding and + // differentiates them from cluster border lines) + // + + double currentRadius = clusterings * (radius_delta + RADIUS_DISTANCE) + RADIUS_INNER; + Element extension = SVGUtil.svgCircleSegment(svgp, 0, 0, offsetAngle, alpha, currentRadius, currentRadius + RADIUS_SELECTION); + extension.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_UNPAIRED_CLASS); + elems.add(extension); + + if(segment.isUnpaired()) { + if(policy.isSelected(segment)) { + SVGUtil.addCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS); + } + else { + // Remove highlight + SVGUtil.removeCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS); + } + } + else { + int idx = policy.indexOfSegment(segment); + if(idx >= 0) { + String color = style.getColorSet(StyleLibrary.PLOT).getColor(idx); + extension.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_FILL_PROPERTY + ":" + color); + } + else { + // Remove styling + extension.removeAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE); + } + } + + visLayer.appendChild(extension); + + // calculate angle for next segment + offsetAngle += alpha + SEGMENT_MIN_SEP_ANGLE; + } + } + + private void redrawSelection() { + final StyleLibrary style = context.getStyleLibrary(); + LOG.debug("Updating selection only."); + for(Entry<Segment, List<Element>> entry : segmentToElements.entrySet()) { + Segment segment = entry.getKey(); + // The selection marker is the extra element in the list + Element extension = entry.getValue().get(segments.getClusterings()); + if(segment.isUnpaired()) { + if(policy.isSelected(segment)) { + SVGUtil.addCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS); + } + else { + // Remove highlight + SVGUtil.removeCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS); + } + } + else { + int idx = policy.indexOfSegment(segment); + if(idx >= 0) { + String color = style.getColorSet(StyleLibrary.PLOT).getColor(idx); + extension.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_FILL_PROPERTY + ":" + color); + } + else { + // Remove styling + extension.removeAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE); + } + } + } + } + + /** + * Creates a gradient over a set of colors + * + * @param shades number of colors in the gradient + * @param colors colors for the gradient + * @return array of colors for CSS + */ + protected String[] makeGradient(int shades, String[] colors) { + if(shades <= colors.length) { + return colors; + } + + // Convert SVG colors into AWT colors for math + Color[] cols = new Color[colors.length]; + for(int i = 0; i < colors.length; i++) { + cols[i] = SVGUtil.stringToColor(colors[i]); + if(cols[i] == null) { + throw new AbortException("Error parsing color: " + colors[i]); + } + } + + // Step size + double increment = (cols.length - 1.) / shades; + + String[] colorShades = new String[shades]; + + for(int s = 0; s < shades; s++) { + final int ppos = Math.min((int) Math.floor(increment * s), cols.length); + final int npos = Math.min((int) Math.ceil(increment * s), cols.length); + if(ppos == npos) { + colorShades[s] = colors[ppos]; + } + else { + Color prev = cols[ppos]; + Color next = cols[npos]; + final double mix = (increment * s - ppos) / (npos - ppos); + final int r = (int) ((1 - mix) * prev.getRed() + mix * next.getRed()); + final int g = (int) ((1 - mix) * prev.getGreen() + mix * next.getGreen()); + final int b = (int) ((1 - mix) * prev.getBlue() + mix * next.getBlue()); + colorShades[s] = SVGUtil.colorToString(((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)); + } + } + + return colorShades; + } + + protected Element drawClusteringInfo() { + Element thumbnail = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); + + // build thumbnail + int startRadius = 4; + int singleHeight = 12; + int margin = 4; + int radius = segments.getClusterings() * (singleHeight + margin) + startRadius; + + SVGUtil.setAtt(thumbnail, SVGConstants.SVG_HEIGHT_ATTRIBUTE, radius); + + for(int i = 0; i < segments.getClusterings(); i++) { + double innerRadius = i * singleHeight + margin * i + startRadius; + Element clr = SVGUtil.svgCircleSegment(svgp, radius - startRadius, radius - startRadius, Math.PI * 1.5, Math.PI * 0.5, innerRadius, innerRadius + singleHeight); + // FIXME: Use StyleLibrary + clr.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1"); + clr.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0"); + clr.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "1.0"); + + String labelText = segments.getClusteringDescription(i); + Element label = svgp.svgText(radius + startRadius, radius - innerRadius - startRadius, labelText); + thumbnail.appendChild(label); + + thumbnail.appendChild(clr); + } + + return thumbnail; + } + + protected void segmentHover(Segment segment, int ringid, boolean active) { + if(active) { + // abort if this are the unclustered pairs + if(segment.isNone()) { + return; + } + if(LOG.isDebugging()) { + LOG.debug("Hover on segment: " + segment + " unpaired: " + segment.isUnpaired()); + } + + if(!segment.isUnpaired()) { + // + // STANDARD CLUSTER SEGMENT + // highlight all ring segments in this clustering and this cluster + // + // highlight all corresponding ring Segments + for(Entry<Segment, List<Element>> entry : segmentToElements.entrySet()) { + Segment other = entry.getKey(); + // Same cluster in same clustering? + if(other.get(ringid) != segment.get(ringid)) { + continue; + } + Element ringSegment = entry.getValue().get(ringid); + SVGUtil.addCSSClass(ringSegment, CLR_HOVER_CLASS); + } + } + else { + // + // UNPAIRED SEGMENT + // highlight all ring segments in this clustering responsible for + // unpaired + // segment + // + // get the paired segments corresponding to the unpaired segment + List<Segment> paired = segments.getPairedSegments(segment); + + for(Segment other : paired) { + Element ringSegment = segmentToElements.get(other).get(ringid); + SVGUtil.addCSSClass(ringSegment, CLR_HOVER_CLASS); + } + } + } + else { + for(List<Element> elems : segmentToElements.values()) { + for(Element current : elems) { + SVGUtil.removeCSSClass(current, CLR_HOVER_CLASS); + } + } + } + } + + protected void segmentClick(Segment segment, Event evt, boolean dblClick) { + MouseEvent mouse = (MouseEvent) evt; + + // CTRL (add) pressed? + boolean ctrl = false; + if(mouse.getCtrlKey()) { + ctrl = true; + } + + // Unselect others on double click + if(dblClick) { + policy.deselectAllSegments(); + } + policy.select(segment, ctrl); + // update stylePolicy + context.setStylingPolicy(policy); + } + + /** + * Proxy element to connect signals. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + private class SegmentListenerProxy implements EventListener { + /** + * Mouse double click time window in milliseconds + * + * TODO: does Batik have double click events? + */ + public static final int EVT_DBLCLICK_DELAY = 350; + + /** + * Segment we are attached to + */ + private Segment id; + + /** + * Segment ring we are + */ + private int ringid; + + /** + * For detecting double clicks. + */ + private long lastClick = 0; + + /** + * Constructor. + * + * @param id Segment id + * @param ringid Ring id + */ + public SegmentListenerProxy(Segment id, int ringid) { + super(); + this.id = id; + this.ringid = ringid; + } + + @Override + public void handleEvent(Event evt) { + if(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE.equals(evt.getType())) { + segmentHover(id, ringid, true); + } + if(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE.equals(evt.getType())) { + segmentHover(id, ringid, false); + } + if(SVGConstants.SVG_CLICK_EVENT_TYPE.equals(evt.getType())) { + // Check Double Click + boolean dblClick = false; + long time = java.util.Calendar.getInstance().getTimeInMillis(); + if(time - lastClick <= EVT_DBLCLICK_DELAY) { + dblClick = true; + } + lastClick = time; + + segmentClick(id, evt, dblClick); + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/SegmentsStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/SegmentsStylingPolicy.java new file mode 100644 index 00000000..7e38d951 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/SegmentsStylingPolicy.java @@ -0,0 +1,324 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.TreeMap; + +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs; +import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segment; +import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segments; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * Styling policy to communicate the segment selection to other visualizers. + * + * TODO: Remove "implements Result" + * + * @author Sascha Goldhofer + * @author Erich Schubert + */ +public class SegmentsStylingPolicy implements ClassStylingPolicy { + /** + * The segments we use for visualization + */ + protected final Segments segments; + + /** + * Selected segments + */ + protected ArrayList<Segment> selectedSegments = new ArrayList<>(); + + /** + * Segments indirectly selected + */ + protected TreeMap<Segment, Segment> indirectSelections = new TreeMap<>(); + + /** + * Not selected IDs that will be drawn in default colors. + */ + protected ModifiableDBIDs unselectedObjects = DBIDUtil.newHashSet(); + + /** + * Color library (only used in compatibility mode) + */ + // TODO: move to abstract super class? + ColorLibrary colorset = null; + + /** + * Constructor. + * + * @param segments Segments + */ + public SegmentsStylingPolicy(Segments segments) { + super(); + this.segments = segments; + + // get all selectable segments + for(Segment segment : segments) { + // store segmentID + if(!segment.isUnpaired()) { + // and store their get all objects + if(segment.getDBIDs() != null) { + unselectedObjects.addDBIDs(segment.getDBIDs()); + } + } + } + } + + /** + * Assign the style library, for compatibility color styling. + * + * FIXME: handle this more generally + * + * @param style Style library + */ + public void setStyleLibrary(StyleLibrary style) { + this.colorset = style.getColorSet(StyleLibrary.PLOT); + } + + /** + * Test whether a segment is selected. + * + * @param segment Segment to test + * @return true when selected + */ + public boolean isSelected(Segment segment) { + return selectedSegments.contains(segment) || indirectSelections.containsValue(segment); + } + + @Override + public int getStyleForDBID(DBIDRef id) { + Iterator<Segment> s = selectedSegments.iterator(); + for(int i = 0; s.hasNext(); i++) { + Segment seg = s.next(); + DBIDs ids = seg.getDBIDs(); + if(ids != null && ids.contains(id)) { + return i; + } + } + return -2; + } + + @Override + public int getColorForDBID(DBIDRef id) { + int style = getStyleForDBID(id); + if(colorset != null) { + // FIXME: add caching + return SVGUtil.stringToColor(colorset.getColor(style)).getRGB(); + } + else { + return 0; + } + } + + @Override + // -2=grau, -1=schwarz, 0+=farben + public int getMinStyle() { + return -2; + } + + @Override + public int getMaxStyle() { + return selectedSegments.size(); + } + + @Override + public DBIDIter iterateClass(int cnum) { + // unselected + if(cnum == -2) { + return unselectedObjects.iter(); + } + else if(cnum == -1) { + return DBIDUtil.EMPTYDBIDS.iter(); + } + // colors + DBIDs ids = selectedSegments.get(cnum).getDBIDs(); + return (ids != null) ? ids.iter() : DBIDUtil.EMPTYDBIDS.iter(); + } + + @Override + public int classSize(int cnum) { + // unselected + if(cnum == -2) { + return unselectedObjects.size(); + } + else if(cnum == -1) { + return 0; + } + // colors + DBIDs ids = selectedSegments.get(cnum).getDBIDs(); + return (ids != null) ? ids.size() : 0; + } + + /** + * Adds or removes the given segment to the selection. Depending on the + * clustering and cluster selected and the addToSelection option given, the + * current selection will be modified. This method is called by clicking on a + * segment and ring and the CTRL-button status. + * + * Adding selections does only work on the same clustering and cluster, else a + * new selection will be added. + * + * @param segment the selected element representing a segment ring (specific + * clustering) + * @param addToSelection flag for adding segment to current selection + */ + public void select(Segment segment, boolean addToSelection) { + // abort if segment represents pairs inNone. Would select all segments... + if(segment.isNone()) { + return; + } + if(!addToSelection) { + deselectAllSegments(); + } + + // get selected segments + if(segment.isUnpaired()) { + // check if all segments are selected + if(addToSelection) { + boolean allSegmentsSelected = true; + for(Segment other : segments.getPairedSegments(segment)) { + if(!isSelected(other)) { + allSegmentsSelected = false; + break; + } + } + + // if all are selected, deselect all + if(allSegmentsSelected) { + deselectSegment(segment); + return; + } + } + if(isSelected(segment)) { + deselectSegment(segment); + } + else { + selectSegment(segment); + } + } + else { + // an object segment was selected + if(isSelected(segment)) { + deselectSegment(segment); + } + else { + selectSegment(segment); + } + } + } + + /** + * Deselect all currently selected segments + */ + public void deselectAllSegments() { + while(selectedSegments.size() > 0) { + deselectSegment(selectedSegments.get(selectedSegments.size() - 1)); + } + } + + /** + * Deselect a segment + * + * @param segment Segment to deselect + */ + protected void deselectSegment(Segment segment) { + if(segment.isUnpaired()) { + ArrayList<Segment> remove = new ArrayList<>(); + // remove all object segments associated with unpaired segment from + // selection list + for(Entry<Segment, Segment> entry : indirectSelections.entrySet()) { + if(entry.getValue() == segment) { + remove.add(entry.getKey()); + } + } + + for(Segment other : remove) { + indirectSelections.remove(other); + deselectSegment(other); + } + } + else { + // check if deselected object Segment has a unpaired segment highlighted + Segment unpaired = indirectSelections.get(segment); + if(unpaired != null) { + // remove highlight + deselectSegment(unpaired); + } + if(selectedSegments.remove(segment)) { + if(segment.getDBIDs() != null) { + unselectedObjects.addDBIDs(segment.getDBIDs()); + } + } + } + } + + /** + * Select a segment + * + * @param segment Segment to select + */ + protected void selectSegment(Segment segment) { + if(segment.isUnpaired()) { + // remember selected unpaired segment + for(Segment other : segments.getPairedSegments(segment)) { + indirectSelections.put(other, segment); + selectSegment(other); + } + } + else { + if(!selectedSegments.contains(segment)) { + selectedSegments.add(segment); + if(segment.getDBIDs() != null) { + unselectedObjects.removeDBIDs(segment.getDBIDs()); + } + } + } + } + + /** + * Get the index of a selected segment. + * + * @param segment Segment to find + * @return Index, or -1 + */ + public int indexOfSegment(Segment segment) { + return selectedSegments.indexOf(segment); + } + + @Override + public String getMenuName() { + return "Pair segments styling policy"; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/package-info.java new file mode 100755 index 00000000..8fb5e781 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for inspecting cluster differences using pair counting segments.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AbstractParallelVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AbstractParallelVisualization.java new file mode 100644 index 00000000..3ac02de2 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AbstractParallelVisualization.java @@ -0,0 +1,170 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.ProjectionParallel; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; + +/** + * Abstract base class for parallel visualizations. + * + * @author Robert Rödler + * @author Erich Schubert + * + * @param <NV> Data type in relation + */ +public abstract class AbstractParallelVisualization<NV> extends AbstractVisualization { + /** + * The current projection + */ + final protected ProjectionParallel proj; + + /** + * The representation we visualize + */ + final protected Relation<NV> relation; + + /** + * margin + */ + final double[] margins; + + /** + * Space between two axes + */ + protected double axsep; + + /** + * viewbox size + */ + final double[] size; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public AbstractParallelVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height); + this.proj = (ProjectionParallel) proj; + this.relation = task.getRelation(); + margins = new double[] { 0.05 * StyleLibrary.SCALE, 0.1 * StyleLibrary.SCALE, 0.05 * StyleLibrary.SCALE, 0.1 * StyleLibrary.SCALE }; + double ratio = (width * StyleLibrary.SCALE - margins[0] - margins[2]) / (height * StyleLibrary.SCALE - margins[1] - margins[3]); + size = new double[] { ratio * StyleLibrary.SCALE, StyleLibrary.SCALE }; + recalcAxisPositions(); + } + + @Override + public void fullRedraw() { + this.layer = setupCanvas(svgp, this.proj, getWidth(), getHeight()); + } + + /** + * Utility function to setup a canvas element for the visualization. + * + * @param svgp Plot element + * @param proj Projection to use + * @param width Width + * @param height Height + * @return wrapper element with appropriate view box. + */ + public Element setupCanvas(SVGPlot svgp, ProjectionParallel proj, double width, double height) { + Element layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); + final String transform = SVGUtil.makeMarginTransform(width, height, size[0], size[1], margins[0], margins[1], margins[2], margins[3]); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + return layer; + } + + /** + * Get width of main canvas. + * + * @return Width + */ + protected double getSizeX() { + return size[0]; + } + + protected double getSizeY() { + return size[1]; + } + + protected double getMarginLeft() { + return margins[0]; + } + + protected double getMarginTop() { + return margins[1]; + } + + /** + * Distance between axes. + * + * @return Axis separation + */ + protected double getAxisSep() { + return axsep; + } + + /** + * Recalculate axis positions, in particular after projection changes. + */ + private void recalcAxisPositions() { + axsep = size[0] / (proj.getVisibleDimensions() - 1.); + } + + /** + * Get the position of visible axis d + * + * @param d Visible axis number + * @return Position + */ + protected double getVisibleAxisX(double d) { + return d * axsep; + } + + @Override + public void visualizationChanged(VisualizationItem item) { + super.visualizationChanged(item); + if(item == proj) { + recalcAxisPositions(); + svgp.requestRedraw(this.task, this); + return; + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisReorderVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisReorderVisualization.java new file mode 100644 index 00000000..ce24ad2a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisReorderVisualization.java @@ -0,0 +1,300 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGArrow; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Interactive SVG-Elements for reordering the axes. + * + * @author Robert Rödler + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class AxisReorderVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Dimension Ordering Tool"; + + /** + * Constructor, adhering to + */ + public AxisReorderVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), AxisReorderVisualization.this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT); + context.addVis(p, task); + } + } + + /** + * Instance for a particular plot. + * + * @author Robert Rödler + * @author Erich Schubert + */ + public class Instance extends AbstractParallelVisualization<NumberVector> { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String SELECTDIMENSIONORDER = "SelectDimensionOrder"; + + /** + * CSS class for a tool button + */ + public static final String SDO_BUTTON = "DObutton"; + + /** + * CSS class for a button border + */ + public static final String SDO_BORDER = "DOborder"; + + /** + * CSS class for a button cross + */ + public static final String SDO_ARROW = "DOarrow"; + + /** + * Currently selected dimension. Use -1 to not have a dimension selected. + */ + private int selecteddim = -1; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + addCSSClasses(svgp); + final int dim = proj.getVisibleDimensions(); + + final double controlsize = 0.025 * getSizeY(); + final double buttonsize = 0.75 * controlsize; + final double padding = 0.125 * controlsize; + final double arrowsize = .75 * buttonsize; + final double ypos = getSizeY() + getMarginTop() * .5 + controlsize; + final double spacing = 0.9 * controlsize; + + Element back = svgp.svgRect(-controlsize * .5, ypos, getSizeX() + controlsize, controlsize); + SVGUtil.addCSSClass(back, SELECTDIMENSIONORDER); + layer.appendChild(back); + + if(selecteddim < 0) { + // Nothing selected + for(int i = 0; i < dim; i++) { + final double xpos = getVisibleAxisX(i); + if(i > 0) { + Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.LEFT, xpos - spacing, ypos + controlsize * .5, arrowsize); + SVGUtil.addCSSClass(arrow, SDO_ARROW); + layer.appendChild(arrow); + Element button = svgp.svgRect(xpos - spacing - buttonsize * .5, ypos + padding, buttonsize, buttonsize); + SVGUtil.addCSSClass(button, SDO_BUTTON); + addEventListener(button, i, SVGArrow.LEFT); + layer.appendChild(button); + } + { + Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.DOWN, xpos, ypos + controlsize * .5, arrowsize); + SVGUtil.addCSSClass(arrow, SDO_ARROW); + layer.appendChild(arrow); + Element button = svgp.svgRect(xpos - buttonsize * .5, ypos + padding, buttonsize, buttonsize); + SVGUtil.addCSSClass(button, SDO_BUTTON); + addEventListener(button, i, SVGArrow.DOWN); + layer.appendChild(button); + } + if(i < dim - 1) { + Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.RIGHT, xpos + spacing, ypos + controlsize * .5, arrowsize); + SVGUtil.addCSSClass(arrow, SDO_ARROW); + layer.appendChild(arrow); + Element button = svgp.svgRect(xpos + spacing - buttonsize * .5, ypos + padding, buttonsize, buttonsize); + SVGUtil.addCSSClass(button, SDO_BUTTON); + addEventListener(button, i, SVGArrow.RIGHT); + layer.appendChild(button); + } + } + } + else { + for(int i = 0; i < dim; i++) { + { + Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.DOWN, getVisibleAxisX(i), ypos + controlsize * .5, arrowsize); + SVGUtil.addCSSClass(arrow, SDO_ARROW); + layer.appendChild(arrow); + Element button = svgp.svgRect(getVisibleAxisX(i) - buttonsize * .5, ypos + padding, buttonsize, buttonsize); + SVGUtil.addCSSClass(button, SDO_BUTTON); + addEventListener(button, i, SVGArrow.DOWN); + layer.appendChild(button); + } + if(i > 0.) { + Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.UP, getVisibleAxisX(i - .5), ypos + controlsize * .5, arrowsize); + SVGUtil.addCSSClass(arrow, SDO_ARROW); + layer.appendChild(arrow); + Element button = svgp.svgRect(getVisibleAxisX(i - .5) - buttonsize * .5, ypos + padding, buttonsize, buttonsize); + SVGUtil.addCSSClass(button, SDO_BUTTON); + addEventListener(button, i, SVGArrow.UP); + layer.appendChild(button); + } + } + } + } + + /** + * Add an event listener to the Element + * + * @param tag Element to add the listener + * @param i represented axis + */ + private void addEventListener(final Element tag, final int i, final SVGArrow.Direction j) { + EventTarget targ = (EventTarget) tag; + targ.addEventListener(SVGConstants.SVG_EVENT_CLICK, new EventListener() { + @Override + public void handleEvent(Event evt) { + if(selecteddim < 0) { + switch(j){ + case DOWN: + selecteddim = i; + break; + case LEFT: + int prev = i - 1; + while(prev >= 0 && !proj.isAxisVisible(prev)) { + prev -= 1; + } + proj.swapAxes(i, prev); + break; + case RIGHT: + int next = i + 1; + while(next < proj.getInputDimensionality() - 1 && !proj.isAxisVisible(next)) { + next += 1; + } + proj.swapAxes(i, next); + break; + default: + break; + } + } + else { + switch(j){ + case DOWN: + proj.swapAxes(selecteddim, i); + selecteddim = -1; + break; + case UP: + if(selecteddim != i) { + proj.moveAxis(selecteddim, i); + } + selecteddim = -1; + break; + default: + break; + } + } + // Notify + context.visChanged(proj); + } + }, false); + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + if(!svgp.getCSSClassManager().contains(SELECTDIMENSIONORDER)) { + CSSClass cls = new CSSClass(this, SELECTDIMENSIONORDER); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.1); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLUE_VALUE); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(SDO_BORDER)) { + CSSClass cls = new CSSClass(this, SDO_BORDER); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_GREY_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) / 3.0); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(SDO_BUTTON)) { + CSSClass cls = new CSSClass(this, SDO_BUTTON); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.01); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREY_VALUE); + cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(SDO_ARROW)) { + CSSClass cls = new CSSClass(this, SDO_ARROW); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_DARKGREY_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) / 3); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + svgp.addCSSClassOrLogError(cls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisVisibilityVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisVisibilityVisualization.java new file mode 100644 index 00000000..1e12f87c --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisVisibilityVisualization.java @@ -0,0 +1,298 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Layer for controlling axis visbility in parallel coordinates. + * + * @author Robert Rödler + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class AxisVisibilityVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Axis Visibility"; + + /** + * Constructor, adhering to + */ + public AxisVisibilityVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), AxisVisibilityVisualization.this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT); + context.addVis(p, task); + } + } + + /** + * Instance for a particular data set. + * + * @author Robert Rödler + * @author Erich Schubert + */ + public class Instance extends AbstractParallelVisualization<NumberVector> { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String SELECTAXISVISIBILITY = "SelectAxisVisibility"; + + /** + * CSS class for a tool button + */ + public static final String SAV_BUTTON = "SAVbutton"; + + /** + * CSS class for a button border + */ + public static final String SAV_BORDER = "SAVborder"; + + /** + * CSS class for a button cross + */ + public static final String SAV_CROSS = "SAVbuttoncross"; + + /** + * Active area size + */ + double controlsize; + + /** + * Button size + */ + double buttonsize; + + /** + * Vertical position + */ + double ypos; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + final int dim = proj.getInputDimensionality(); + addCSSClasses(svgp); + controlsize = 0.025 * getSizeY(); + buttonsize = 0.75 * controlsize; + ypos = getSizeY() + getMarginTop() * .5; + + // Background + Element back = svgp.svgRect(-controlsize * .5, ypos - controlsize * .5 + buttonsize * .5, getSizeX() + controlsize, controlsize); + SVGUtil.addCSSClass(back, SELECTAXISVISIBILITY); + layer.appendChild(back); + + // Previous visible dimension. + for(int i = 0, hidden = 0, vax = 0; i <= dim; i++) { + if(i < dim && !proj.isAxisVisible(i)) { + hidden += 1; + continue; + } + // Add button for showing hidden dimensions: + if(hidden > 0) { + makeButtonsForHidden(vax, i - hidden, hidden, dim); + hidden = 0; + } + // Add buttons for current dimension + if(i < dim) { + makeButtonForVisible(i, vax); + vax++; + } + } + } + + /** + * Make a button for a visible axis + * + * @param anum Axis number + * @param apos Axis position in plot + */ + protected void makeButtonForVisible(int anum, int apos) { + final double xpos = getVisibleAxisX(apos) - buttonsize * .5; + + Element border = svgp.svgRect(xpos, ypos, buttonsize, buttonsize); + SVGUtil.addCSSClass(border, SAV_BORDER); + layer.appendChild(border); + + SVGPath path = new SVGPath(); + final double qs = controlsize * .5; + final double cs = controlsize * .125; + path.moveTo(xpos + cs, ypos + cs); + path.relativeLineTo(qs, qs); + path.relativeMoveTo(0, -qs); + path.relativeLineTo(-qs, qs); + Element cross = path.makeElement(svgp); + SVGUtil.addCSSClass(cross, SAV_CROSS); + layer.appendChild(cross); + + Element rect = svgp.svgRect(xpos, ypos, buttonsize, buttonsize); + SVGUtil.addCSSClass(rect, SAV_BUTTON); + addEventListener(rect, anum); + layer.appendChild(rect); + } + + /** + * Insert buttons for hidden dimensions. + * + * @param vnum Column number (= next visible axis number) + * @param first First invisible axis + * @param count Number of invisible axes + * @param dim Number of total dimensions + */ + private void makeButtonsForHidden(final int vnum, final int first, final int count, final int dim) { + final double lpos, rpos; + if(vnum == 0) { + lpos = -getMarginLeft(); + } + else { + lpos = getVisibleAxisX(vnum - 1); + } + if(first + count + 1 >= dim) { + rpos = getWidth() + getMarginLeft(); + } + else { + rpos = getVisibleAxisX(vnum); + } + final double step = (rpos - lpos) / (count + 1.0); + for(int j = 0; j < count; j++) { + final double apos = lpos + (j + 1) * step - buttonsize * .5; + Element border = svgp.svgRect(apos, ypos, buttonsize, buttonsize); + SVGUtil.addCSSClass(border, SAV_BORDER); + layer.appendChild(border); + + Element rect = svgp.svgRect(apos, ypos, buttonsize, buttonsize); + SVGUtil.addCSSClass(rect, SAV_BUTTON); + addEventListener(rect, first + j); + layer.appendChild(rect); + } + } + + /** + * Add an event listener to the Element + * + * @param tag Element to add the listener + * @param axis Axis number (including hidden axes) + */ + private void addEventListener(final Element tag, final int axis) { + EventTarget targ = (EventTarget) tag; + targ.addEventListener(SVGConstants.SVG_EVENT_CLICK, new EventListener() { + @Override + public void handleEvent(Event evt) { + if(proj.getVisibleDimensions() > 2) { + proj.toggleAxisVisible(axis); + context.visChanged(proj); + } + } + }, false); + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + if(!svgp.getCSSClassManager().contains(SELECTAXISVISIBILITY)) { + CSSClass cls = new CSSClass(this, SELECTAXISVISIBILITY); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.1); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLUE_VALUE); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(SAV_BORDER)) { + CSSClass cls = new CSSClass(this, SAV_BORDER); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_GREY_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(SAV_BUTTON)) { + CSSClass cls = new CSSClass(this, SAV_BUTTON); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.01); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREY_VALUE); + cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(SAV_CROSS)) { + CSSClass cls = new CSSClass(this, SAV_CROSS); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .75); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(cls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/BoundingBoxVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/BoundingBoxVisualization.java new file mode 100644 index 00000000..6b3671d1 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/BoundingBoxVisualization.java @@ -0,0 +1,254 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.SamplingResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Draw spatial objects (except vectors!) + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +// TODO: draw filled instead? +public class BoundingBoxVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Spatial objects"; + + /** + * Constructor. + */ + public BoundingBoxVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + if(!TypeUtil.SPATIAL_OBJECT.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), BoundingBoxVisualization.this); + task.level = VisualizationTask.LEVEL_DATA; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY | VisualizationTask.ON_SAMPLE); + context.addVis(p, task); + } + } + + /** + * Instance for a particular data set. + * + * @author Robert Rödler + */ + public class Instance extends AbstractParallelVisualization<SpatialComparable>implements DataStoreListener { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String DATALINE = "Databox"; + + /** + * Sample we visualize. + */ + private SamplingResult sample; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.sample = ResultUtil.getSamplingResult(relation); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + final DBIDs sam = sample.getSample(); + StylingPolicy sp = context.getStylingPolicy(); + final StyleLibrary style = context.getStyleLibrary(); + final LineStyleLibrary lines = style.lines(); + final double width = .5 * style.getLineWidth(StyleLibrary.PLOT) * MathUtil.min(.5, 2. / MathUtil.log2(sam.size())); + if(sp instanceof ClassStylingPolicy) { + ClassStylingPolicy csp = (ClassStylingPolicy) sp; + final int min = csp.getMinStyle(); + String[] keys = new String[csp.getMaxStyle() - min]; + for(int c = min; c < csp.getMaxStyle(); c++) { + String key = keys[c - min] = DATALINE + "_" + c; + if(!svgp.getCSSClassManager().contains(key)) { + CSSClass cls = new CSSClass(this, key); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + lines.formatCSSClass(cls, c, width); + svgp.addCSSClassOrLogError(cls); + } + } + for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) { + final int c = csp.getStyleForDBID(iter) + min; + if(c < 0) { + continue; // No style. Display differently? + } + Element line = drawLine(iter); + if(line == null) { + continue; + } + SVGUtil.addCSSClass(line, keys[c]); + layer.appendChild(line); + } + } + else { + // No classes available, but individually colored + if(!svgp.getCSSClassManager().contains(DATALINE)) { + CSSClass cls = new CSSClass(this, DATALINE); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + lines.formatCSSClass(cls, -1, width); + svgp.addCSSClassOrLogError(cls); + } + StringBuilder buf = new StringBuilder().append(SVGConstants.CSS_STROKE_PROPERTY).append(':'); + final int prefix = buf.length(); + for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) { + Element line = drawLine(iter); + if(line == null) { + continue; + } + SVGUtil.addCSSClass(line, DATALINE); + // assign color + buf.delete(prefix, buf.length()); + buf.append(SVGUtil.colorToString(sp.getColorForDBID(iter))); + line.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, buf.toString()); + layer.appendChild(line); + } + } + svgp.updateStyleElement(); + } + + /** + * Draw a single line. + * + * @param iter Object reference + * @return Line element + */ + private Element drawLine(DBIDRef iter) { + SVGPath path = new SVGPath(); + final SpatialComparable obj = relation.get(iter); + final int dims = proj.getVisibleDimensions(); + boolean drawn = false; + int valid = 0; /* run length of valid values */ + double prevpos = Double.NaN; + for(int i = 0; i < dims; i++) { + final int d = proj.getDimForAxis(i); + double minPos = proj.fastProjectDataToRenderSpace(obj.getMin(d), i); + // NaN handling: + if(minPos != minPos) { + valid = 0; + continue; + } + ++valid; + if(valid > 1) { + if(valid == 2) { + path.moveTo(getVisibleAxisX(d - 1), prevpos); + } + path.lineTo(getVisibleAxisX(d), minPos); + drawn = true; + } + prevpos = minPos; + } + valid = 0; + for(int i = dims - 1; i >= 0; i--) { + final int d = proj.getDimForAxis(i); + double maxPos = proj.fastProjectDataToRenderSpace(obj.getMax(d), i); + // NaN handling: + if(maxPos != maxPos) { + valid = 0; + continue; + } + ++valid; + if(valid > 1) { + if(valid == 2) { + path.moveTo(getVisibleAxisX(d + 1), prevpos); + } + path.lineTo(getVisibleAxisX(d), maxPos); + drawn = true; + } + prevpos = maxPos; + } + if(!drawn) { + return null; // Not enough data. + } + return path.makeElement(svgp); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/LineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/LineVisualization.java new file mode 100644 index 00000000..a13bdb74 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/LineVisualization.java @@ -0,0 +1,226 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.SamplingResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Generates data lines. + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class LineVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Data lines"; + + /** + * Constructor. + */ + public LineVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), LineVisualization.this); + task.level = VisualizationTask.LEVEL_DATA; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY | VisualizationTask.ON_SAMPLE); + context.addVis(p, task); + } + } + + /** + * Instance for a particular data set. + * + * @author Robert Rödler + */ + public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String DATALINE = "Dataline"; + + /** + * Sample we visualize. + */ + private SamplingResult sample; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.sample = ResultUtil.getSamplingResult(relation); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + final DBIDs sam = sample.getSample(); + StylingPolicy sp = context.getStylingPolicy(); + final StyleLibrary style = context.getStyleLibrary(); + final LineStyleLibrary lines = style.lines(); + final double width = style.getLineWidth(StyleLibrary.PLOT) * MathUtil.min(.5, 2. / MathUtil.log2(sam.size())); + if(sp instanceof ClassStylingPolicy) { + ClassStylingPolicy csp = (ClassStylingPolicy) sp; + final int min = csp.getMinStyle(); + String[] keys = new String[csp.getMaxStyle() - min]; + for(int c = min; c < csp.getMaxStyle(); c++) { + String key = keys[c - min] = DATALINE + "_" + c; + if(!svgp.getCSSClassManager().contains(key)) { + CSSClass cls = new CSSClass(this, key); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + lines.formatCSSClass(cls, c, width); + svgp.addCSSClassOrLogError(cls); + } + } + for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) { + final int c = csp.getStyleForDBID(iter) + min; + if(c < 0) { + continue; // No style. Display differently? + } + Element line = drawLine(iter); + if(line == null) { + continue; + } + SVGUtil.addCSSClass(line, keys[c]); + layer.appendChild(line); + } + } + else { + // No classes available, but individually colored + if(!svgp.getCSSClassManager().contains(DATALINE)) { + CSSClass cls = new CSSClass(this, DATALINE); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + lines.formatCSSClass(cls, -1, width); + svgp.addCSSClassOrLogError(cls); + } + StringBuilder buf = new StringBuilder().append(SVGConstants.CSS_STROKE_PROPERTY).append(':'); + final int prefix = buf.length(); + for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) { + Element line = drawLine(iter); + if(line == null) { + continue; + } + SVGUtil.addCSSClass(line, DATALINE); + // assign color + buf.delete(prefix, buf.length()); + buf.append(SVGUtil.colorToString(sp.getColorForDBID(iter))); + line.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, buf.toString()); + layer.appendChild(line); + } + } + svgp.updateStyleElement(); + } + + /** + * Draw a single line. + * + * @param iter Object reference + * @return Line element + */ + private Element drawLine(DBIDRef iter) { + SVGPath path = new SVGPath(); + double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(iter)); + boolean drawn = false; + int valid = 0; /* run length of valid values */ + for(int i = 0; i < yPos.length; i++) { + // NaN handling: + if(yPos[i] != yPos[i]) { + valid = 0; + continue; + } + ++valid; + if(valid > 1) { + if(valid == 2) { + path.moveTo(getVisibleAxisX(i - 1), yPos[i - 1]); + } + path.lineTo(getVisibleAxisX(i), yPos[i]); + drawn = true; + } + } + if(!drawn) { + return null; // Not enough data. + } + return path.makeElement(svgp); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/ParallelAxisVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/ParallelAxisVisualization.java new file mode 100644 index 00000000..4f35faf6 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/ParallelAxisVisualization.java @@ -0,0 +1,216 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Generates a SVG-Element containing axes, including labeling. + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class ParallelAxisVisualization extends AbstractVisFactory { + /** + * Class logger + */ + private static final Logging LOG = Logging.getLogger(ParallelAxisVisualization.class); + + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Parallel Axes"; + + /** + * Constructor. + */ + public ParallelAxisVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), this); + task.level = VisualizationTask.LEVEL_BACKGROUND; + context.addVis(p, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return true; + } + + /** + * Instance. + * + * @author Robert Rödler + * + * @apiviz.uses SVGSimpleLinearAxis + */ + // TODO: split into interactive / non-interactive parts? + public class Instance extends AbstractParallelVisualization<NumberVector> { + /** + * Axis label class. + */ + public static final String AXIS_LABEL = "paxis-label"; + + /** + * Clickable area for the axis. + */ + public static final String INVERTEDAXIS = "paxis-button"; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + final StyleLibrary style = context.getStyleLibrary(); + addCSSClasses(svgp); + final int dim = proj.getInputDimensionality(); + for(int i = 0, vdim = 0; i < dim; i++) { + if(!proj.isAxisVisible(i)) { + continue; + } + final int truedim = proj.getDimForAxis(i); + final double axisX = getVisibleAxisX(vdim); + try { + if(!proj.isAxisInverted(vdim)) { + SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getAxisScale(i), axisX, getSizeY(), axisX, 0, SVGSimpleLinearAxis.LabelStyle.ENDLABEL, style); + } + else { + SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getAxisScale(i), axisX, 0, axisX, getSizeY(), SVGSimpleLinearAxis.LabelStyle.ENDLABEL, style); + } + } + catch(CSSNamingConflict e) { + LOG.warning("Conflict in CSS naming for axes.", e); + continue; + } + // Get axis label + final String label = RelationUtil.getColumnLabel(relation, truedim); + // Add axis label + Element text = svgp.svgText(axisX, -.7 * getMarginTop(), label); + SVGUtil.setCSSClass(text, AXIS_LABEL); + // TODO: find a reliable way for sizing axis labels. + if(dim > 10) { + SVGUtil.setAtt(text, SVGConstants.SVG_TEXT_LENGTH_ATTRIBUTE, getAxisSep() * 0.95); + SVGUtil.setAtt(text, SVGConstants.SVG_LENGTH_ADJUST_ATTRIBUTE, SVGConstants.SVG_SPACING_AND_GLYPHS_VALUE); + } + layer.appendChild(text); + // TODO: Split into background + clickable layer. + Element button = svgp.svgRect(axisX - getAxisSep() * .475, -getMarginTop(), .95 * getAxisSep(), .5 * getMarginTop()); + SVGUtil.setCSSClass(button, INVERTEDAXIS); + addEventListener(button, truedim); + layer.appendChild(button); + vdim++; + } + } + + /** + * Add the main CSS classes. + * + * @param svgp Plot to draw to + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + if(!svgp.getCSSClassManager().contains(AXIS_LABEL)) { + CSSClass cls = new CSSClass(this, AXIS_LABEL); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL)); + cls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL)); + cls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL)); + cls.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.SVG_MIDDLE_VALUE); + svgp.addCSSClassOrLogError(cls); + } + if(!svgp.getCSSClassManager().contains(INVERTEDAXIS)) { + CSSClass cls = new CSSClass(this, INVERTEDAXIS); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.1); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREY_VALUE); + svgp.addCSSClassOrLogError(cls); + } + } + + /** + * Add an event listener to the Element. + * + * @param tag Element to add the listener + * @param truedim Tool number for the Element + */ + private void addEventListener(final Element tag, final int truedim) { + EventTarget targ = (EventTarget) tag; + targ.addEventListener(SVGConstants.SVG_EVENT_CLICK, new EventListener() { + @Override + public void handleEvent(Event evt) { + proj.toggleDimInverted(truedim); + context.visChanged(proj); + } + }, false); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterOutlineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterOutlineVisualization.java new file mode 100644 index 00000000..1a660bf9 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterOutlineVisualization.java @@ -0,0 +1,308 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Iterator; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +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.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.DoubleMinMax; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +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.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.DoubleParameter; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterHullVisualization; + +/** + * Generates a SVG-Element that visualizes the area covered by a cluster. + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +// TODO: make parameterizable: rounded. +public class ClusterOutlineVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Cluster Hull (Parallel Coordinates)"; + + /** + * Settings + */ + Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public ClusterOutlineVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + // We use the style library, not individual clusterings! + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, ClusterOutlineVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 1; + task.initDefaultVisibility(false); + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance + * + * @author Robert Rödler + * @author Erich Schubert + */ + public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String CLUSTERAREA = "Clusteroutline"; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + final StylingPolicy spol = context.getStylingPolicy(); + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + final ClusterStylingPolicy cpol = (ClusterStylingPolicy) spol; + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) cpol.getClustering(); + + int dim = proj.getVisibleDimensions(); + + DoubleMinMax[] mms = DoubleMinMax.newArray(dim); + DoubleMinMax[] midmm = DoubleMinMax.newArray(dim - 1); + + // Heuristic value for transparency: + double baseopacity = .5; + + Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator(); + for(int cnum = 0; cnum < clustering.getAllClusters().size(); cnum++) { + Cluster<?> clus = ci.next(); + final DBIDs ids = clus.getIDs(); + if(ids.size() < 1) { + continue; + } + for(int i = 0; i < dim; i++) { + mms[i].reset(); + if(i < dim - 1) { + midmm[i].reset(); + } + } + + // Process points + // TODO: do this just once, cache the result somewhere appropriately? + for(DBIDIter id = ids.iter(); id.valid(); id.advance()) { + double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(id)); + for(int i = 0; i < dim; i++) { + mms[i].put(yPos[i]); + if(i > 0) { + midmm[i - 1].put((yPos[i] + yPos[i - 1]) / 2.); + } + } + } + + SVGPath path = new SVGPath(); + if(!settings.bend) { + // Straight lines + for(int i = 0; i < dim; i++) { + path.drawTo(getVisibleAxisX(i), mms[i].getMax()); + if(i < dim - 1) { + path.drawTo(getVisibleAxisX(i + .5), midmm[i].getMax()); + } + } + for(int i = dim - 1; i >= 0; i--) { + if(i < dim - 1) { + path.drawTo(getVisibleAxisX(i + .5), midmm[i].getMin()); + } + path.drawTo(getVisibleAxisX(i), mms[i].getMin()); + } + } + else { + // Maxima + path.drawTo(getVisibleAxisX(0), mms[0].getMax()); + for(int i = 1; i < dim; i++) { + path.quadTo(getVisibleAxisX(i - .5), midmm[i - 1].getMax(), getVisibleAxisX(i), mms[i].getMax()); + } + // Minima + path.drawTo(getVisibleAxisX(dim - 1), mms[dim - 1].getMin()); + for(int i = dim - 1; i > 0; i--) { + path.quadTo(getVisibleAxisX(i - .5), midmm[i - 1].getMin(), getVisibleAxisX(i - 1), mms[i - 1].getMin()); + } + } + path.close(); + + // TODO: improve the visualization by adjusting the opacity by the + // cluster extends on each axis (maybe use a horizontal gradient?) + double weight = 0.; + for(int i = 0; i < dim; i++) { + weight += mms[i].getDiff(); + } + weight = (weight > 0.) ? (dim * StyleLibrary.SCALE) / weight : 1.; + + Element intervals = path.makeElement(svgp); + addCSSClasses(svgp, cpol.getStyleForCluster(clus), baseopacity * weight * ids.size() / relation.size()); + SVGUtil.addCSSClass(intervals, CLUSTERAREA + cnum); + layer.appendChild(intervals); + } + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + * @param clusterID Cluster ID to style + * @param opac Opacity + */ + private void addCSSClasses(SVGPlot svgp, int clusterID, double opac) { + final StyleLibrary style = context.getStyleLibrary(); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + CSSClass cls = new CSSClass(this, CLUSTERAREA + clusterID); + final String color = colors.getColor(clusterID); + + // cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, + // context.getStyleLibrary().getLineWidth(StyleLibrary.PLOT) / 2.0); + // cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, opac); + + svgp.addCSSClassOrLogError(cls); + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Option string to draw straight lines for hull. + */ + public static final OptionID STRAIGHT_ID = new OptionID("parallel.clusteroutline.straight", "Draw straight lines"); + + /** + * Alpha value + */ + double alpha = Double.POSITIVE_INFINITY; + + /** + * Use bend curves + */ + private boolean bend = true; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + DoubleParameter alphaP = new DoubleParameter(ClusterHullVisualization.Parameterizer.ALPHA_ID, Double.POSITIVE_INFINITY); + if(config.grab(alphaP)) { + alpha = alphaP.doubleValue(); + } + + Flag bendP = new Flag(STRAIGHT_ID); + if(config.grab(bendP)) { + bend = bendP.isFalse(); + } + } + + @Override + protected ClusterOutlineVisualization makeInstance() { + return new ClusterOutlineVisualization(this); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterParallelMeanVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterParallelMeanVisualization.java new file mode 100644 index 00000000..6e0dc167 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterParallelMeanVisualization.java @@ -0,0 +1,193 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Iterator; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.model.MeanModel; +import de.lmu.ifi.dbs.elki.data.model.MedoidModel; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization; + +/** + * Generates a SVG-Element that visualizes cluster means. + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class ClusterParallelMeanVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Cluster Means"; + + /** + * Constructor. + */ + public ClusterParallelMeanVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), ClusterParallelMeanVisualization.this); + task.level = VisualizationTask.LEVEL_DATA + 1; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance. + * + * @author Robert Rödler + * + */ + public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String CLUSTERMEAN = "Clustermean"; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + final StylingPolicy spol = context.getStylingPolicy(); + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering(); + if(clustering.getAllClusters().size() == 0) { + return; + } + + final StyleLibrary style = context.getStyleLibrary(); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator(); + for(int cnum = 0; cnum < clustering.getAllClusters().size(); cnum++) { + Model model = ci.next().getModel(); + NumberVector mean = null; + try { + if(model instanceof MeanModel) { + mean = ((MeanModel) model).getMean(); + } + else if(model instanceof MedoidModel) { + mean = relation.get(((MedoidModel) model).getMedoid()); + } + } + catch(ObjectNotFoundException e) { + continue; // Element not found. + } + if(mean == null) { + continue; + } + double[] pmean = proj.fastProjectDataToRenderSpace(mean); + + SVGPath path = new SVGPath(); + for(int i = 0; i < pmean.length; i++) { + path.drawTo(getVisibleAxisX(i), pmean[i]); + } + Element meanline = path.makeElement(svgp); + + String cnam = CLUSTERMEAN + cnum; + if(!svgp.getCSSClassManager().contains(cnam)) { + CSSClass cls = new CSSClass(this, cnam); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * 2.); + + final String color = colors.getColor(cnum); + + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + + svgp.addCSSClassOrLogError(cls); + } + SVGUtil.addCSSClass(meanline, cnam); + layer.appendChild(meanline); + } + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/package-info.java new file mode 100755 index 00000000..d82a4e1d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for clustering results based on parallel coordinates.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/RTreeParallelVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/RTreeParallelVisualization.java new file mode 100644 index 00000000..338729f4 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/RTreeParallelVisualization.java @@ -0,0 +1,252 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index; + +/*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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.spatial.SpatialUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.index.tree.spatial.SpatialEntry; +import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTree; +import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTreeNode; +import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.rstar.RStarTreeNode; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.ProjectionParallel; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeMBRVisualization; + +/** + * Visualize the of an R-Tree based index. + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class RTreeParallelVisualization extends AbstractVisFactory { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String INDEX = "parallelrtree"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "R-Tree Index MBRs"; + + /** + * Settings + */ + protected Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public RTreeParallelVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance<RStarTreeNode, SpatialEntry>(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNewSiblings(context, start, AbstractRStarTree.class, ParallelPlotProjector.class, // + new VisualizationTree.Handler2<AbstractRStarTree<RStarTreeNode, SpatialEntry, ?>, ParallelPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, AbstractRStarTree<RStarTreeNode, SpatialEntry, ?> tree, ParallelPlotProjector<?> p) { + final VisualizationTask task = new VisualizationTask(NAME, context, (Result) tree, p.getRelation(), RTreeParallelVisualization.this); + task.level = VisualizationTask.LEVEL_BACKGROUND + 2; + task.default_visibility = false; + context.addVis((Result) tree, task); + context.addVis(p, task); + } + }); + } + + /** + * Instance for a particular data set and tree + * + * @author Robert Rödler + * + * @apiviz.has AbstractRStarTree oneway - - visualizes + * + * @param <N> Tree node type + * @param <E> Tree entry type + */ + public class Instance<N extends AbstractRStarTreeNode<N, E>, E extends SpatialEntry> extends AbstractParallelVisualization<NumberVector>implements DataStoreListener { + /** + * The tree we visualize + */ + protected AbstractRStarTree<N, E, ?> tree; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + @SuppressWarnings("unchecked") + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.tree = AbstractRStarTree.class.cast(task.getResult()); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + addCSSClasses(svgp); + E root = tree.getRootEntry(); + visualizeRTreeEntry(svgp, layer, proj, tree, root, 0, 0); + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + final ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + for(int i = 0; i < tree.getHeight(); i++) { + if(!svgp.getCSSClassManager().contains(INDEX + i)) { + CSSClass cls = new CSSClass(this, INDEX + i); + + // Relative depth of this level. 1.0 = toplevel + final double relDepth = 1. - (((double) i) / tree.getHeight()); + if(settings.fill) { + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.2); + } + else { + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + } + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + svgp.addCSSClassOrLogError(cls); + } + } + svgp.updateStyleElement(); + } + + /** + * Recursively draw the MBR rectangles. + * + * @param svgp SVG Plot + * @param layer Layer + * @param proj Projection + * @param rtree Rtree to visualize + * @param entry Current entry + * @param depth Current depth + */ + private void visualizeRTreeEntry(SVGPlot svgp, Element layer, ProjectionParallel proj, AbstractRStarTree<? extends N, E, ?> rtree, E entry, int depth, int step) { + final int dim = proj.getVisibleDimensions(); + double[] min = proj.fastProjectDataToRenderSpace(SpatialUtil.getMin(entry)); + double[] max = proj.fastProjectDataToRenderSpace(SpatialUtil.getMax(entry)); + assert(min.length == dim && max.length == dim); + SVGPath path = new SVGPath(); + for(int i = 0; i < dim; i++) { + path.drawTo(getVisibleAxisX(i), Math.max(min[i], max[i])); + } + for(int i = dim - 1; i >= 0; i--) { + path.drawTo(getVisibleAxisX(i), Math.min(min[i], max[i])); + } + path.close(); + + Element intervals = path.makeElement(svgp); + + SVGUtil.addCSSClass(intervals, INDEX + depth); + layer.appendChild(intervals); + + if(!entry.isLeafEntry()) { + N node = rtree.getNode(entry); + for(int i = 0; i < node.getNumEntries(); i++) { + E child = node.getEntry(i); + if(!child.isLeafEntry()) { + visualizeRTreeEntry(svgp, layer, proj, rtree, child, depth + 1, ++step); + } + } + } + } + + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + protected boolean fill = true; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + Flag fillF = new Flag(TreeMBRVisualization.Parameterizer.FILL_ID); + fillF.setDefaultValue(Boolean.TRUE); + if(config.grab(fillF)) { + fill = fillF.isTrue(); + } + } + + @Override + protected RTreeParallelVisualization makeInstance() { + return new RTreeParallelVisualization(this); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/package-info.java new file mode 100755 index 00000000..b6de9cf4 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for index structure based on parallel coordinates.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/package-info.java new file mode 100755 index 00000000..b4fffa74 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Visualizers based on parallel coordinates.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionAxisRangeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionAxisRangeVisualization.java new file mode 100644 index 00000000..5f56e774 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionAxisRangeVisualization.java @@ -0,0 +1,180 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.HyperBoundingBox; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.result.RangeSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization; + +/** + * Visualizer for generating an SVG-Element representing the selected range. + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class SelectionAxisRangeVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Selection Axis Range"; + + /** + * Constructor. + */ + public SelectionAxisRangeVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionAxisRangeVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 1; + task.addUpdateFlags(VisualizationTask.ON_SELECTION); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance + * + * @author Robert Rödler + * + * @apiviz.has RangeSelection oneway - - visualizes + */ + public class Instance extends AbstractParallelVisualization<NumberVector> { + /** + * CSS Class for the range marker + */ + public static final String MARKER = "selectionAxisRange"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + // Class for the cube + if(!svgp.getCSSClassManager().contains(MARKER)) { + CSSClass cls = new CSSClass(this, MARKER); + cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + + svgp.addCSSClassOrLogError(cls); + } + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + addCSSClasses(svgp); + DBIDSelection selContext = context.getSelection(); + if(!(selContext instanceof RangeSelection)) { + return; + } + HyperBoundingBox range = ((RangeSelection) selContext).getRanges(); + if(range == null) { + return; + } + + // Project: + final int dims = range.getDimensionality(); + double[] min = new double[dims]; + double[] max = new double[dims]; + for(int d = 0; d < dims; d++) { + min[d] = range.getMin(d); + max[d] = range.getMax(d); + } + min = proj.fastProjectDataToRenderSpace(min); + max = proj.fastProjectDataToRenderSpace(max); + + final int vdim = proj.getVisibleDimensions(); + for(int vd = 0; vd < vdim; vd++) { + final int ad = proj.getDimForVisibleAxis(vd); + final double amin = Math.min(min[ad], max[ad]); + final double amax = Math.max(min[ad], max[ad]); + if(amin > Double.MIN_VALUE && amax < Double.MAX_VALUE) { + Element rect = svgp.svgRect(getVisibleAxisX(vd) - (0.01 * StyleLibrary.SCALE), amin, 0.02 * StyleLibrary.SCALE, amax - amin); + SVGUtil.addCSSClass(rect, MARKER); + layer.appendChild(rect); + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionLineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionLineVisualization.java new file mode 100644 index 00000000..b4107147 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionLineVisualization.java @@ -0,0 +1,198 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization; + +/** + * Visualizer for generating SVG-Elements representing the selected objects + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class SelectionLineVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Selection Line"; + + /** + * Constructor. + */ + public SelectionLineVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionLineVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 1; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance + * + * @author Robert Rödler + * + * @apiviz.has DBIDSelection oneway - - visualizes + */ + public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener { + /** + * CSS Class for the range marker + */ + public static final String MARKER = "SelectionLine"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + addCSSClasses(svgp); + DBIDSelection selContext = context.getSelection(); + if(selContext != null) { + DBIDs selection = selContext.getSelectedIds(); + + for(DBIDIter iter = selection.iter(); iter.valid(); iter.advance()) { + Element marker = drawLine(iter); + if(marker == null) { + continue; + } + SVGUtil.addCSSClass(marker, MARKER); + layer.appendChild(marker); + } + } + } + + /** + * Draw a single line. + * + * @param iter Object reference + * @return SVG Element + */ + private Element drawLine(DBIDRef iter) { + SVGPath path = new SVGPath(); + double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(iter)); + boolean draw = false, drawprev = false, drawn = false; + for(int i = 0; i < yPos.length; i++) { + // NaN handling: + if(yPos[i] != yPos[i]) { + draw = false; + drawprev = false; + continue; + } + if(draw) { + if(drawprev) { + path.moveTo(getVisibleAxisX(i - 1), yPos[i - 1]); + drawprev = false; + } + path.lineTo(getVisibleAxisX(i), yPos[i]); + drawn = true; + } + else { + drawprev = true; + } + draw = true; + } + if(!drawn) { + return null; // Not enough data. + } + return path.makeElement(svgp); + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + // Class for the cube + if(!svgp.getCSSClassManager().contains(MARKER)) { + CSSClass cls = new CSSClass(this, MARKER); + cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * 2.); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(cls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolAxisRangeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolAxisRangeVisualization.java new file mode 100644 index 00000000..3dd96007 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolAxisRangeVisualization.java @@ -0,0 +1,314 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.data.ModifiableHyperBoundingBox; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.result.RangeSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization; + +/** + * Tool-Visualization for the tool to select axis ranges + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class SelectionToolAxisRangeVisualization extends AbstractVisFactory { + /** + * The logger for this class. + */ + private static final Logging LOG = Logging.getLogger(SelectionToolAxisRangeVisualization.class); + + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Axis Range Selection"; + + /** + * Constructor. + */ + public SelectionToolAxisRangeVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolAxisRangeVisualization.this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_SELECTION); + task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT); + task.initDefaultVisibility(false); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance + * + * @author Robert Rödler + * + * @apiviz.has RangeSelection oneway - - updates + */ + public class Instance extends AbstractParallelVisualization<NumberVector>implements DragableArea.DragListener { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + private static final String CSS_RANGEMARKER = "selectionAxisRangeMarker"; + + /** + * Element for selection rectangle + */ + private Element rtag; + + /** + * Element for the rectangle to add listeners + */ + private Element etag; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + addCSSClasses(svgp); + + // rtag: tag for the selected rect + rtag = svgp.svgElement(SVGConstants.SVG_G_TAG); + SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER); + layer.appendChild(rtag); + + // etag: sensitive area + DragableArea drag = new DragableArea(svgp, -.1 * getMarginLeft(), -.1 * getMarginTop(), getSizeX() + getMarginLeft() * .2, getSizeY() + getMarginTop() * .2, this); + etag = drag.getElement(); + layer.appendChild(etag); + } + + /** + * Delete the children of the element + * + * @param container SVG-Element + */ + private void deleteChildren(Element container) { + while(container.hasChildNodes()) { + container.removeChild(container.getLastChild()); + } + } + + /** + * Set the selected ranges and the mask for the actual dimensions in the + * context + * + * @param x1 min x-value + * @param x2 max x-value + * @param y1 min y-value + * @param y2 max y-value + */ + private void updateSelectionRectKoordinates(double x1, double x2, double y1, double y2, ModifiableHyperBoundingBox ranges) { + final int dims = proj.getVisibleDimensions(); + int minaxis = dims + 1; + int maxaxis = -1; + { + int i = 0; + while(i < dims) { + double axx = getVisibleAxisX(i); + if(x1 < axx || x2 < axx) { + minaxis = i; + break; + } + i++; + } + while(i <= dims) { + double axx = getVisibleAxisX(i); + if(x2 < axx && x1 < axx) { + maxaxis = i; + break; + } + i++; + } + } + double z1 = Math.max(Math.min(y1, y2), 0); + double z2 = Math.min(Math.max(y1, y2), getSizeY()); + for(int i = minaxis; i < maxaxis; i++) { + double v1 = proj.fastProjectRenderToDataSpace(z1, i); + double v2 = proj.fastProjectRenderToDataSpace(z2, i); + final int ddim = proj.getDimForVisibleAxis(i); + if(LOG.isDebugging()) { + LOG.debug("Axis " + i + " dimension " + ddim + " " + v1 + " to " + v2); + } + ranges.setMin(ddim, Math.min(v1, v2)); + ranges.setMax(ddim, Math.max(v1, v2)); + } + } + + @Override + public boolean startDrag(SVGPoint startPoint, Event evt) { + return true; + } + + @Override + public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + deleteChildren(rtag); + double x = Math.min(startPoint.getX(), dragPoint.getX()); + double y = Math.min(startPoint.getY(), dragPoint.getY()); + double width = Math.abs(startPoint.getX() - dragPoint.getX()); + double height = Math.abs(startPoint.getY() - dragPoint.getY()); + rtag.appendChild(svgp.svgRect(x, y, width, height)); + return true; + } + + @Override + public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + deleteChildren(rtag); + if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) { + updateSelection(proj, startPoint, dragPoint); + } + return true; + } + + /** + * Update the selection in the context. + * + * @param proj The projection + * @param p1 First Point of the selected rectangle + * @param p2 Second Point of the selected rectangle + */ + private void updateSelection(Projection proj, SVGPoint p1, SVGPoint p2) { + DBIDSelection selContext = context.getSelection(); + ModifiableDBIDs selection; + if(selContext != null) { + selection = DBIDUtil.newHashSet(selContext.getSelectedIds()); + } + else { + selection = DBIDUtil.newHashSet(); + } + ModifiableHyperBoundingBox ranges; + + if(p1 == null || p2 == null) { + LOG.warning("no rect selected: p1: " + p1 + " p2: " + p2); + } + else { + double x1 = Math.min(p1.getX(), p2.getX()); + double x2 = Math.max(p1.getX(), p2.getX()); + double y1 = Math.max(p1.getY(), p2.getY()); + double y2 = Math.min(p1.getY(), p2.getY()); + + int dim = proj.getInputDimensionality(); + if(selContext instanceof RangeSelection) { + ranges = ((RangeSelection) selContext).getRanges(); + } + else { + ranges = new ModifiableHyperBoundingBox(dim, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + } + updateSelectionRectKoordinates(x1, x2, y1, y2, ranges); + + selection.clear(); + + candidates: for(DBIDIter iditer = relation.iterDBIDs(); iditer.valid(); iditer.advance()) { + NumberVector dbTupel = relation.get(iditer); + for(int d = 0; d < dim; d++) { + final double min = ranges.getMin(d), max = ranges.getMax(d); + if(max < Double.POSITIVE_INFINITY && min > Double.NEGATIVE_INFINITY) { + if(dbTupel.doubleValue(d) < min || dbTupel.doubleValue(d) > max) { + continue candidates; + } + } + } + selection.add(iditer); + } + context.setSelection(new RangeSelection(selection, ranges)); + } + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + protected void addCSSClasses(SVGPlot svgp) { + // Class for the range marking + if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) { + final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER); + final StyleLibrary style = context.getStyleLibrary(); + rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE)); + rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE)); + svgp.addCSSClassOrLogError(rcls); + } + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolLineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolLineVisualization.java new file mode 100644 index 00000000..aa22a267 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolLineVisualization.java @@ -0,0 +1,337 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.geom.Line2D; + +import org.apache.batik.dom.events.DOMMouseEvent; +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.ids.HashSetModifiableDBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization; + +/** + * Tool-Visualization for the tool to select objects + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance - - «create» + */ +public class SelectionToolLineVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Object Selection"; + + /** + * Input modes + * + * @apiviz.exclude + */ + private enum Mode { + REPLACE, ADD, INVERT + } + + /** + * Constructor. + */ + public SelectionToolLineVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class); + for(; it.valid(); it.advance()) { + ParallelPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolLineVisualization.this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_SELECTION); + task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT); + task.initDefaultVisibility(false); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Robert Rödler + * + * @apiviz.has DBIDSelection oneway - - updates + */ + public class Instance extends AbstractParallelVisualization<NumberVector>implements DragableArea.DragListener { + /** + * CSS class of the selection rectangle while selecting. + */ + private static final String CSS_RANGEMARKER = "selectionRangeMarker"; + + /** + * Element for selection rectangle + */ + Element rtag; + + /** + * Element for the rectangle to add listeners + */ + Element etag; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + super.fullRedraw(); + addCSSClasses(svgp); + + rtag = svgp.svgElement(SVGConstants.SVG_G_TAG); + SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER); + layer.appendChild(rtag); + + // etag: sensitive area + DragableArea drag = new DragableArea(svgp, -.1 * getMarginLeft(), -.5 * getMarginTop(), getSizeX() + .2 * getMarginLeft(), getMarginTop() * 1.5 + getSizeY(), this); + etag = drag.getElement(); + layer.appendChild(etag); + } + + /** + * Delete the children of the element + * + * @param container SVG-Element + */ + private void deleteChildren(Element container) { + while(container.hasChildNodes()) { + container.removeChild(container.getLastChild()); + } + } + + @Override + public boolean startDrag(SVGPoint startPoint, Event evt) { + return true; + } + + @Override + public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + deleteChildren(rtag); + double x = Math.min(startPoint.getX(), dragPoint.getX()); + double y = Math.min(startPoint.getY(), dragPoint.getY()); + double width = Math.abs(startPoint.getX() - dragPoint.getX()); + double height = Math.abs(startPoint.getY() - dragPoint.getY()); + rtag.appendChild(svgp.svgRect(x, y, width, height)); + return true; + } + + @Override + public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + Mode mode = getInputMode(evt); + deleteChildren(rtag); + if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) { + updateSelection(mode, startPoint, dragPoint); + } + return true; + } + + /** + * Get the current input mode, on each mouse event. + * + * @param evt Mouse event. + * @return current input mode + */ + private Mode getInputMode(Event evt) { + if(evt instanceof DOMMouseEvent) { + DOMMouseEvent domme = (DOMMouseEvent) evt; + // TODO: visual indication of mode possible? + if(domme.getShiftKey()) { + return Mode.ADD; + } + else if(domme.getCtrlKey()) { + return Mode.INVERT; + } + else { + return Mode.REPLACE; + } + } + // Default mode is replace. + return Mode.REPLACE; + } + + /** + * Updates the selection in the context.<br> + * + * @param mode Input mode + * @param p1 first point of the selected rectangle + * @param p2 second point of the selected rectangle + */ + private void updateSelection(Mode mode, SVGPoint p1, SVGPoint p2) { + DBIDSelection selContext = context.getSelection(); + // Note: we rely on SET semantics below! + final HashSetModifiableDBIDs selection; + if(selContext == null || mode == Mode.REPLACE) { + selection = DBIDUtil.newHashSet(); + } + else { + selection = DBIDUtil.newHashSet(selContext.getSelectedIds()); + } + int[] axisrange = getAxisRange(Math.min(p1.getX(), p2.getX()), Math.max(p1.getX(), p2.getX())); + DBIDs ids = ResultUtil.getSamplingResult(relation).getSample(); + for(DBIDIter iter = ids.iter(); iter.valid(); iter.advance()) { + double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(iter)); + if(checkSelected(axisrange, yPos, Math.max(p1.getX(), p2.getX()), Math.min(p1.getX(), p2.getX()), Math.max(p1.getY(), p2.getY()), Math.min(p1.getY(), p2.getY()))) { + if(mode == Mode.INVERT) { + if(!selection.contains(iter)) { + selection.add(iter); + } + else { + selection.remove(iter); + } + } + else { + // In REPLACE and ADD, add objects. + // The difference was done before by not re-using the selection. + // Since we are using a set, we can just add in any case. + selection.add(iter); + } + } + } + context.setSelection(new DBIDSelection(selection)); + } + + private int[] getAxisRange(double x1, double x2) { + final int dim = proj.getVisibleDimensions(); + int minaxis = 0; + int maxaxis = 0; + boolean minx = true; + boolean maxx = false; + int count = -1; + for(int i = 0; i < dim; i++) { + if(minx && getVisibleAxisX(i) > x1) { + minaxis = count; + minx = false; + maxx = true; + } + if(maxx && (getVisibleAxisX(i) > x2 || i == dim - 1)) { + maxaxis = count + 1; + if(i == dim - 1 && getVisibleAxisX(i) <= x2) { + maxaxis++; + } + break; + } + count = i; + } + return new int[] { minaxis, maxaxis }; + } + + private boolean checkSelected(int[] ar, double[] yPos, double x1, double x2, double y1, double y2) { + final int dim = proj.getVisibleDimensions(); + if(ar[0] < 0) { + ar[0] = 0; + } + if(ar[1] >= dim) { + ar[1] = dim - 1; + } + for(int i = ar[0] + 1; i <= ar[1] - 1; i++) { + if(yPos[i] <= y1 && yPos[i] >= y2) { + return true; + } + } + Line2D.Double idline1 = new Line2D.Double(getVisibleAxisX(ar[0]), yPos[ar[0]], getVisibleAxisX(ar[0] + 1), yPos[ar[0] + 1]); + Line2D.Double idline2 = new Line2D.Double(getVisibleAxisX(ar[1] - 1), yPos[ar[1] - 1], getVisibleAxisX(ar[1]), yPos[ar[1]]); + Line2D.Double rectline1 = new Line2D.Double(x2, y1, x1, y1); + Line2D.Double rectline2 = new Line2D.Double(x2, y1, x2, y2); + Line2D.Double rectline3 = new Line2D.Double(x2, y2, x1, y2); + if(idline1.intersectsLine(rectline1) || idline1.intersectsLine(rectline2) || idline1.intersectsLine(rectline3)) { + return true; + } + Line2D.Double rectline4 = new Line2D.Double(x1, y1, x1, y2); + if(idline2.intersectsLine(rectline1) || idline2.intersectsLine(rectline4) || idline2.intersectsLine(rectline3)) { + return true; + } + return false; + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + protected void addCSSClasses(SVGPlot svgp) { + // Class for the range marking + if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) { + final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER); + final StyleLibrary style = context.getStyleLibrary(); + rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE)); + rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE)); + svgp.addCSSClassOrLogError(rcls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/package-info.java new file mode 100755 index 00000000..cc12b74a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for object selection based on parallel projections.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractScatterplotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractScatterplotVisualization.java new file mode 100644 index 00000000..13225426 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractScatterplotVisualization.java @@ -0,0 +1,124 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.result.SamplingResult; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; + +/** + * Default class to handle 2D projected visualizations. + * + * @author Erich Schubert + * + * @apiviz.landmark + * @apiviz.has Projection2D + */ +public abstract class AbstractScatterplotVisualization extends AbstractVisualization { + /** + * The current projection + */ + final protected Projection2D proj; + + /** + * The representation we visualize + */ + final protected Relation<? extends NumberVector> rel; + + /** + * The DBID sample + */ + final protected SamplingResult sample; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public AbstractScatterplotVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height); + this.proj = (Projection2D) proj; + this.rel = task.getRelation(); + this.sample = task.updateOnAny(VisualizationTask.ON_SAMPLE) ? ResultUtil.getSamplingResult(rel) : null; + } + + /** + * Setup our canvas. + * + * @return Canvas + */ + protected Element setupCanvas() { + final double margin = context.getStyleLibrary().getSize(StyleLibrary.MARGIN); + this.layer = setupCanvas(svgp, this.proj, margin, getWidth(), getHeight()); + return layer; + } + + /** + * Utility function to setup a canvas element for the visualization. + * + * @param svgp Plot element + * @param proj Projection to use + * @param margin Margin to use + * @param width Width + * @param height Height + * @return wrapper element with appropriate view box. + */ + public static Element setupCanvas(SVGPlot svgp, Projection2D proj, double margin, double width, double height) { + final CanvasSize canvas = proj.estimateViewport(); + final double sizex = canvas.getDiffX(); + final double sizey = canvas.getDiffY(); + String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin) + " translate(" + SVGUtil.fmt(sizex * .5) + " " + SVGUtil.fmt(sizey * .5) + ")"; + + final Element layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + return layer; + } + + @Override + public void visualizationChanged(VisualizationItem item) { + super.visualizationChanged(item); + if(item == proj) { + svgp.requestRedraw(this.task, this); + return; + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractTooltipVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractTooltipVisualization.java new file mode 100644 index 00000000..313b2dff --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractTooltipVisualization.java @@ -0,0 +1,190 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; + +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; + +/** + * General base class for a tooltip visualizer. + * + * @author Erich Schubert + */ +// TODO: can we improve performance by not adding as many hovers? +public abstract class AbstractTooltipVisualization extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String TOOLTIP_HIDDEN = "tooltip_hidden"; + + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String TOOLTIP_VISIBLE = "tooltip_visible"; + + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String TOOLTIP_STICKY = "tooltip_sticky"; + + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String TOOLTIP_AREA = "tooltip_area"; + + /** + * Our event listener. + */ + EventListener hoverer = new EventListener() { + @Override + public void handleEvent(Event evt) { + handleHoverEvent(evt); + } + }; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public AbstractTooltipVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + } + + @Override + public void fullRedraw() { + setupCanvas(); + setupCSS(svgp); + final StyleLibrary style = context.getStyleLibrary(); + double dotsize = style.getLineWidth(StyleLibrary.PLOT); + + for(DBIDIter id = sample.getSample().iter(); id.valid(); id.advance()) { + double[] v = proj.fastProjectDataToRenderSpace(rel.get(id)); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + Element tooltip = makeTooltip(id, v[0], v[1], dotsize); + SVGUtil.addCSSClass(tooltip, TOOLTIP_HIDDEN); + + // sensitive area. + Element area = svgp.svgRect(v[0] - dotsize, v[1] - dotsize, 2 * dotsize, 2 * dotsize); + SVGUtil.addCSSClass(area, TOOLTIP_AREA); + + EventTarget targ = (EventTarget) area; + targ.addEventListener(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE, hoverer, false); + targ.addEventListener(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE, hoverer, false); + targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, hoverer, false); + + // NOTE: do not change the sequence in which these are inserted! + layer.appendChild(area); + layer.appendChild(tooltip); + } + } + + /** + * Make a tooltip Element for this id. + * + * @param id Id to make a tooltip for + * @param x X position + * @param y Y position + * @param dotsize Size of a dot + * @return Element + */ + protected abstract Element makeTooltip(DBIDRef id, double x, double y, double dotsize); + + /** + * Handle the hover events. + * + * @param evt Event. + */ + protected void handleHoverEvent(Event evt) { + if(evt.getTarget() instanceof Element) { + Element e = (Element) evt.getTarget(); + Node next = e.getNextSibling(); + if(next instanceof Element) { + toggleTooltip((Element) next, evt.getType()); + } + else { + LoggingUtil.warning("Tooltip sibling not found."); + } + } + else { + LoggingUtil.warning("Got event for non-Element?!?"); + } + } + + /** + * Toggle the Tooltip of an element. + * + * @param elem Element + * @param type Event type + */ + protected void toggleTooltip(Element elem, String type) { + String csscls = elem.getAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE); + if(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE.equals(type)) { + if(TOOLTIP_HIDDEN.equals(csscls)) { + SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_VISIBLE); + } + } + else if(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE.equals(type)) { + if(TOOLTIP_VISIBLE.equals(csscls)) { + SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_HIDDEN); + } + } + else if(SVGConstants.SVG_CLICK_EVENT_TYPE.equals(type)) { + if(TOOLTIP_STICKY.equals(csscls)) { + SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_HIDDEN); + } + if(TOOLTIP_HIDDEN.equals(csscls) || TOOLTIP_VISIBLE.equals(csscls)) { + SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_STICKY); + } + } + } + + /** + * Registers the Tooltip-CSS-Class at a SVGPlot. + * + * @param svgp the SVGPlot to register the Tooltip-CSS-Class. + */ + protected abstract void setupCSS(SVGPlot svgp); +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AxisVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AxisVisualization.java new file mode 100644 index 00000000..9fa1bd96 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AxisVisualization.java @@ -0,0 +1,167 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Generates a SVG-Element containing axes, including labeling. + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class AxisVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Axes"; + + /** + * Constructor. + */ + public AxisVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), AxisVisualization.this); + task.level = VisualizationTask.LEVEL_BACKGROUND; + context.addVis(p, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance. + * + * @author Erich Schubert + * @author Remigius Wojdanowski + * + * @apiviz.uses SVGSimpleLinearAxis + * + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + final int dim = RelationUtil.dimensionality(rel); + + // origin + double[] orig = proj.fastProjectScaledToRenderSpace(new double[dim]); + // diagonal point opposite to origin + double[] diag = new double[dim]; + for(int d2 = 0; d2 < dim; d2++) { + diag[d2] = 1; + } + diag = proj.fastProjectScaledToRenderSpace(diag); + // compute angle to diagonal line, used for axis labeling. + double diaga = Math.atan2(diag[1] - orig[1], diag[0] - orig[0]); + + double alfontsize = 1.1 * style.getTextSize(StyleLibrary.AXIS_LABEL); + CSSClass alcls = new CSSClass(AxisVisualization.class, "unmanaged"); + alcls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, SVGUtil.fmt(alfontsize)); + alcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL)); + alcls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL)); + + // draw axes + for(int d = 0; d < dim; d++) { + double[] v = new double[dim]; + v[d] = 1; + // projected endpoint of axis + double[] ax = proj.fastProjectScaledToRenderSpace(v); + boolean righthand = false; + double axa = Math.atan2(ax[1] - orig[1], ax[0] - orig[0]); + if(axa > diaga || (diaga > 0 && axa > diaga + Math.PI)) { + righthand = true; + } + // System.err.println(ax.get(0) + " "+ ax.get(1)+ + // " "+(axa*180/Math.PI)+" "+(diaga*180/Math.PI)); + if(ax[0] != orig[0] || ax[1] != orig[1]) { + try { + SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getScale(d), orig[0], orig[1], ax[0], ax[1], righthand ? SVGSimpleLinearAxis.LabelStyle.RIGHTHAND : SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style); + // TODO: move axis labeling into drawAxis function. + double offx = (righthand ? 1 : -1) * 0.02 * Projection.SCALE; + double offy = (righthand ? 1 : -1) * 0.02 * Projection.SCALE; + Element label = svgp.svgText(ax[0] + offx, ax[1] + offy, RelationUtil.getColumnLabel(rel, d)); + SVGUtil.setAtt(label, SVGConstants.SVG_STYLE_ATTRIBUTE, alcls.inlineCSS()); + SVGUtil.setAtt(label, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, righthand ? SVGConstants.SVG_START_VALUE : SVGConstants.SVG_END_VALUE); + layer.appendChild(label); + } + catch(CSSNamingConflict e) { + throw new RuntimeException("Conflict in CSS naming for axes.", e); + } + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/MarkerVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/MarkerVisualization.java new file mode 100644 index 00000000..6508d9ce --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/MarkerVisualization.java @@ -0,0 +1,164 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualize e.g. a clustering using different markers for different clusters. + * This visualizer is not constraint to clusters. It can in fact visualize any + * kind of result we have a style source for. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class MarkerVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Markers"; + + /** + * Constructor. + */ + public MarkerVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, MarkerVisualization.this); + task.level = VisualizationTask.LEVEL_DATA; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Erich Schubert + * + * @apiviz.uses StylingPolicy + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String DOTMARKER = "dot"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + final MarkerLibrary ml = style.markers(); + final double marker_size = style.getSize(StyleLibrary.MARKERPLOT); + final StylingPolicy spol = context.getStylingPolicy(); + + if(spol instanceof ClassStylingPolicy) { + ClassStylingPolicy cspol = (ClassStylingPolicy) spol; + for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) { + try { + final NumberVector vec = rel.get(iter); + double[] v = proj.fastProjectDataToRenderSpace(vec); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + ml.useMarker(svgp, layer, v[0], v[1], cspol.getStyleForDBID(iter), marker_size); + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + else { + final String FILL = SVGConstants.CSS_FILL_PROPERTY + ":"; + // Color-based styling. Fall back to dots + for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) { + try { + double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter)); + Element dot = svgp.svgCircle(v[0], v[1], marker_size); + SVGUtil.addCSSClass(dot, DOTMARKER); + int col = spol.getColorForDBID(iter); + SVGUtil.setAtt(dot, SVGConstants.SVG_STYLE_ATTRIBUTE, FILL + SVGUtil.colorToString(col)); + layer.appendChild(dot); + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/PolygonVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/PolygonVisualization.java new file mode 100644 index 00000000..796d8f5f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/PolygonVisualization.java @@ -0,0 +1,178 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.spatial.Polygon; +import de.lmu.ifi.dbs.elki.data.spatial.PolygonsObject; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.ArrayListIter; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Renders PolygonsObject in the data set. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class PolygonVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Polygons"; + + /** + * Constructor + */ + public PolygonVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + VisualizationTree.findNewResultVis(context, result, Relation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<Relation<?>, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, Relation<?> rel, ScatterPlotProjector<?> p) { + if(!TypeUtil.POLYGON_TYPE.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + if(RelationUtil.dimensionality(p.getRelation()) != 2) { + return; + } + // Assume that a 2d projector is using the same coordinates as the + // polygons. + final VisualizationTask task = new VisualizationTask(NAME, context, rel, rel, PolygonVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 10; + task.addUpdateFlags(VisualizationTask.ON_DATA); + context.addVis(rel, task); + context.addVis(p, task); + } + }); + } + + /** + * Instance + * + * @author Erich Schubert + * + * @apiviz.has PolygonsObject - - visualizes + */ + public class Instance extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String POLYS = "polys"; + + /** + * The representation we visualize + */ + final protected Relation<PolygonsObject> rep; + + /** + * Constructor. + * + * @param task Task to visualize + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.rep = task.getResult(); // Note: relation was used for projection + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + CSSClass css = new CSSClass(svgp, POLYS); + // TODO: separate fill and line colors? + css.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.POLYGONS)); + css.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.POLYGONS)); + css.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(css); + svgp.updateStyleElement(); + + // draw data + for(DBIDIter iditer = rep.iterDBIDs(); iditer.valid(); iditer.advance()) { + try { + PolygonsObject poly = rep.get(iditer); + if(poly == null) { + continue; + } + SVGPath path = new SVGPath(); + for(Polygon ppoly : poly.getPolygons()) { + Vector first = ppoly.get(0); + double[] f = proj.fastProjectDataToRenderSpace(first.getArrayRef()); + path.moveTo(f[0], f[1]); + for(ArrayListIter<Vector> it = ppoly.iter(); it.valid(); it.advance()) { + if(it.getOffset() == 0) { + continue; + } + double[] p = proj.fastProjectDataToRenderSpace(it.get().getArrayRef()); + path.drawTo(p[0], p[1]); + } + // close path. + path.drawTo(f[0], f[1]); + } + Element e = path.makeElement(svgp); + SVGUtil.addCSSClass(e, POLYS); + layer.appendChild(e); + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/ReferencePointsVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/ReferencePointsVisualization.java new file mode 100644 index 00000000..1f0246e0 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/ReferencePointsVisualization.java @@ -0,0 +1,157 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Iterator; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.ReferencePointsResult; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * The actual visualization instance, for a single projection + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class ReferencePointsVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Reference Points"; + + /** + * Constructor. + */ + public ReferencePointsVisualization() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + VisualizationTree.findNewResultVis(context, result, ReferencePointsResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<ReferencePointsResult<?>, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, ReferencePointsResult<?> rp, ScatterPlotProjector<?> p) { + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + final VisualizationTask task = new VisualizationTask(NAME, context, rp, rel, ReferencePointsVisualization.this); + task.level = VisualizationTask.LEVEL_DATA; + context.addVis(rp, task); + context.addVis(p, task); + } + }); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + /** + * Instance. + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.has ReferencePointsResult oneway - - visualizes + */ + // TODO: add a result listener for the reference points. + public class Instance extends AbstractScatterplotVisualization { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String REFPOINT = "refpoint"; + + /** + * Serves reference points. + */ + protected ReferencePointsResult<? extends NumberVector> result; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.result = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + setupCSS(svgp); + Iterator<? extends NumberVector> iter = result.iterator(); + + final double dotsize = style.getSize(StyleLibrary.REFERENCE_POINTS); + while(iter.hasNext()) { + NumberVector v = iter.next(); + double[] projected = proj.fastProjectDataToRenderSpace(v); + Element dot = svgp.svgCircle(projected[0], projected[1], dotsize); + SVGUtil.addCSSClass(dot, REFPOINT); + layer.appendChild(dot); + } + } + + /** + * Registers the Reference-Point-CSS-Class at a SVGPlot. + * + * @param svgp the SVGPlot to register the -CSS-Class. + */ + private void setupCSS(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + CSSClass refpoint = new CSSClass(svgp, REFPOINT); + refpoint.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.REFERENCE_POINTS)); + svgp.addCSSClassOrLogError(refpoint); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipScoreVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipScoreVisualization.java new file mode 100644 index 00000000..585b5405 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipScoreVisualization.java @@ -0,0 +1,280 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.text.NumberFormat; +import java.util.Locale; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.relation.DoubleRelation; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.ResultHierarchy; +import de.lmu.ifi.dbs.elki.result.outlier.OutlierResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter; +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.constraints.CommonConstraints; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Generates a SVG-Element containing Tooltips. Tooltips remain invisible until + * their corresponding Marker is touched by the cursor and stay visible as long + * as the cursor lingers on the marker. + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class TooltipScoreVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Outlier Score Tooltips"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME_GEN = " Tooltips"; + + /** + * Settings + */ + protected Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public TooltipScoreVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + final ResultHierarchy hier = context.getHierarchy(); + // TODO: we can also visualize other scores! + VisualizationTree.findNewSiblings(context, result, OutlierResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<OutlierResult, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, OutlierResult o, ScatterPlotProjector<?> p) { + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + final VisualizationTask task = new VisualizationTask(o.getLongName() + NAME_GEN, context, o.getScores(), rel, TooltipScoreVisualization.this); + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + task.initDefaultVisibility(false); + context.addVis(o.getScores(), task); + context.addVis(p, task); + } + }); + VisualizationTree.findNewSiblings(context, result, DoubleRelation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<DoubleRelation, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, DoubleRelation r, ScatterPlotProjector<?> p) { + for(Iter<Result> it = hier.iterParents(r); it.valid(); it.advance()) { + if(it.get() instanceof OutlierResult) { + return; // Handled by above case already. + } + } + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + final VisualizationTask task = new VisualizationTask(r.getLongName() + NAME_GEN, context, r, rel, TooltipScoreVisualization.this); + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + task.initDefaultVisibility(false); + context.addVis(r, task); + context.addVis(p, task); + } + }); + VisualizationTree.findNewSiblings(context, result, Relation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<Relation<?>, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, Relation<?> r, ScatterPlotProjector<?> p) { + if(r instanceof DoubleRelation) { + return; // Handled above already. + } + if(!TypeUtil.DOUBLE.isAssignableFromType(r.getDataTypeInformation()) && !TypeUtil.INTEGER.isAssignableFromType(r.getDataTypeInformation())) { + return; + } + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + final VisualizationTask task = new VisualizationTask(r.getLongName() + NAME_GEN, context, r, rel, TooltipScoreVisualization.this); + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + task.initDefaultVisibility(false); + context.addVis(r, task); + context.addVis(p, task); + } + }); + } + + /** + * Instance + * + * @author Remigius Wojdanowski + * @author Erich Schubert + */ + public class Instance extends AbstractTooltipVisualization { + /** + * Number value to visualize + */ + private Relation<? extends Number> result; + + /** + * Font size to use. + */ + private double fontsize; + + /** + * Constructor + * + * @param task Task + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.result = task.getResult(); + final StyleLibrary style = context.getStyleLibrary(); + this.fontsize = 3 * style.getTextSize(StyleLibrary.PLOT); + addListeners(); + } + + @Override + protected Element makeTooltip(DBIDRef id, double x, double y, double dotsize) { + return svgp.svgText(x + dotsize, y + fontsize * 0.07, settings.nf.format(result.get(id).doubleValue())); + } + + /** + * Registers the Tooltip-CSS-Class at a SVGPlot. + * + * @param svgp the SVGPlot to register the Tooltip-CSS-Class. + */ + @Override + protected void setupCSS(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + final double fontsize = style.getTextSize(StyleLibrary.PLOT); + final String fontfamily = style.getFontFamily(StyleLibrary.PLOT); + + CSSClass tooltiphidden = new CSSClass(svgp, TOOLTIP_HIDDEN); + tooltiphidden.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + tooltiphidden.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + tooltiphidden.setStatement(SVGConstants.CSS_DISPLAY_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(tooltiphidden); + + CSSClass tooltipvisible = new CSSClass(svgp, TOOLTIP_VISIBLE); + tooltipvisible.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + tooltipvisible.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + svgp.addCSSClassOrLogError(tooltipvisible); + + CSSClass tooltipsticky = new CSSClass(svgp, TOOLTIP_STICKY); + tooltipsticky.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + tooltipsticky.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + svgp.addCSSClassOrLogError(tooltipsticky); + + // invisible but sensitive area for the tooltip activator + CSSClass tooltiparea = new CSSClass(svgp, TOOLTIP_AREA); + tooltiparea.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE); + tooltiparea.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_NONE_VALUE); + tooltiparea.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0"); + tooltiparea.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + svgp.addCSSClassOrLogError(tooltiparea); + + svgp.updateStyleElement(); + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Number formatter used for visualization + */ + NumberFormat nf = null; + + /** + * Parameter for the gamma-correction. + * + * <p> + * Key: {@code -tooltip.digits} + * </p> + * + * <p> + * Default value: 4 + * </p> + */ + public static final OptionID DIGITS_ID = new OptionID("tooltip.digits", "Number of digits to show (e.g. when visualizing outlier scores)"); + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + IntParameter digitsP = new IntParameter(DIGITS_ID, 4); + digitsP.addConstraint(CommonConstraints.GREATER_EQUAL_ZERO_INT); + + if(config.grab(digitsP)) { + int digits = digitsP.intValue(); + nf = NumberFormat.getInstance(Locale.ROOT); + nf.setGroupingUsed(false); + nf.setMaximumFractionDigits(digits); + } + } + + @Override + protected TooltipScoreVisualization makeInstance() { + return new TooltipScoreVisualization(this); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipStringVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipStringVisualization.java new file mode 100644 index 00000000..d367306d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipStringVisualization.java @@ -0,0 +1,214 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.ClassLabel; +import de.lmu.ifi.dbs.elki.data.ExternalID; +import de.lmu.ifi.dbs.elki.data.LabelList; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBID; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Generates a SVG-Element containing Tooltips. Tooltips remain invisible until + * their corresponding Marker is touched by the cursor and stay visible as long + * as the cursor lingers on the marker. + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class TooltipStringVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME_ID = "ID Tooltips"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME_LABEL = "Object Label Tooltips"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME_CLASS = "Class Label Tooltips"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME_EID = "External ID Tooltips"; + + /** + * Constructor. + */ + public TooltipStringVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object result) { + VisualizationTree.findNewSiblings(context, result, Relation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<Relation<?>, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, Relation<?> rep, ScatterPlotProjector<?> p) { + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + final Class<?> clz = rep.getDataTypeInformation().getRestrictionClass(); + if(DBID.class.isAssignableFrom(clz)) { + final VisualizationTask task = new VisualizationTask(NAME_ID, context, rep, rel, TooltipStringVisualization.this); + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + task.initDefaultVisibility(false); + context.addVis(rep, task); + context.addVis(p, task); + } + if(ClassLabel.class.isAssignableFrom(rep.getDataTypeInformation().getRestrictionClass())) { + final VisualizationTask task = new VisualizationTask(NAME_CLASS, context, rep, rel, TooltipStringVisualization.this); + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + task.initDefaultVisibility(false); + context.addVis(rep, task); + context.addVis(p, task); + } + if(LabelList.class.isAssignableFrom(rep.getDataTypeInformation().getRestrictionClass())) { + final VisualizationTask task = new VisualizationTask(NAME_LABEL, context, rep, rel, TooltipStringVisualization.this); + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + task.initDefaultVisibility(false); + context.addVis(rep, task); + context.addVis(p, task); + } + if(ExternalID.class.isAssignableFrom(rep.getDataTypeInformation().getRestrictionClass())) { + final VisualizationTask task = new VisualizationTask(NAME_EID, context, rep, rel, TooltipStringVisualization.this); + task.tool = true; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + task.initDefaultVisibility(false); + context.addVis(rep, task); + context.addVis(p, task); + } + } + }); + } + + /** + * Instance + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.has Relation oneway - - visualizes + */ + public class Instance extends AbstractTooltipVisualization { + /** + * Number value to visualize + */ + private Relation<?> result; + + /** + * Font size to use. + */ + private double fontsize; + + /** + * Constructor. + * + * @param task Task + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.result = task.getResult(); + final StyleLibrary style = context.getStyleLibrary(); + this.fontsize = 3 * style.getTextSize(StyleLibrary.PLOT); + addListeners(); + } + + @Override + protected Element makeTooltip(DBIDRef id, double x, double y, double dotsize) { + final Object data = result.get(id); + String label = (data == null) ? "null" : data.toString(); + label = (label == "" || label == null) ? "null" : label; + return svgp.svgText(x + dotsize, y + fontsize * 0.07, label); + } + + /** + * Registers the Tooltip-CSS-Class at a SVGPlot. + * + * @param svgp the SVGPlot to register the Tooltip-CSS-Class. + */ + @Override + protected void setupCSS(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + final double fontsize = style.getTextSize(StyleLibrary.PLOT); + final String fontfamily = style.getFontFamily(StyleLibrary.PLOT); + + CSSClass tooltiphidden = new CSSClass(svgp, TOOLTIP_HIDDEN); + tooltiphidden.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + tooltiphidden.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + tooltiphidden.setStatement(SVGConstants.CSS_DISPLAY_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(tooltiphidden); + + CSSClass tooltipvisible = new CSSClass(svgp, TOOLTIP_VISIBLE); + tooltipvisible.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + tooltipvisible.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + svgp.addCSSClassOrLogError(tooltipvisible); + + CSSClass tooltipsticky = new CSSClass(svgp, TOOLTIP_STICKY); + tooltipsticky.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + tooltipsticky.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + svgp.addCSSClassOrLogError(tooltipsticky); + + // invisible but sensitive area for the tooltip activator + CSSClass tooltiparea = new CSSClass(svgp, TOOLTIP_AREA); + tooltiparea.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE); + tooltiparea.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_NONE_VALUE); + tooltiparea.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0"); + tooltiparea.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + svgp.addCSSClassOrLogError(tooltiparea); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterHullVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterHullVisualization.java new file mode 100644 index 00000000..fb4ed69c --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterHullVisualization.java @@ -0,0 +1,403 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.model.CoreObjectsModel; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.data.spatial.Polygon; +import de.lmu.ifi.dbs.elki.data.spatial.SpatialUtil; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.geometry.AlphaShape; +import de.lmu.ifi.dbs.elki.math.geometry.GrahamScanConvexHull2D; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter; +import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.ArrayListIter; +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.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.DoubleParameter; +import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleObjPair; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualizer for generating an SVG-Element containing the convex hull / alpha + * shape of each cluster. + * + * @author Robert Rödler + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class ClusterHullVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Cluster Hull (Scatterplot)"; + + /** + * Settings + */ + Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public ClusterHullVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + // We attach ourselves to the style library, not the clustering, so there is + // only one hull. + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, ClusterHullVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 1; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY); + task.initDefaultVisibility(false); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Robert Rödler + * @author Erich Schubert + * + * @apiviz.has Clustering oneway - - visualizes + * @apiviz.uses GrahamScanConvexHull2D + * @apiviz.uses AlphaShape + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String CLUSTERHULL = "cluster-hull"; + + /** + * Constructor + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StylingPolicy spol = context.getStylingPolicy(); + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + final ClusterStylingPolicy cpol = (ClusterStylingPolicy) spol; + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) cpol.getClustering(); + + // Viewport size, for "relative size" computations + final CanvasSize viewp = proj.estimateViewport(); + double projarea = viewp.getDiffX() * viewp.getDiffY(); + + List<Cluster<Model>> clusters = clustering.getAllClusters(); + List<Cluster<Model>> topc = clustering.getToplevelClusters(); + Hierarchy<Cluster<Model>> hier = clustering.getClusterHierarchy(); + boolean flat = (clusters.size() == topc.size()); + // Heuristic value for transparency: + double baseopacity = flat ? 0.5 : 0.5; + + // Convex hull mode: + if(settings.alpha >= Double.POSITIVE_INFINITY) { + // Build the convex hulls (reusing the hulls of nested clusters!) + Map<Object, DoubleObjPair<Polygon>> hullmap = new HashMap<>(clusters.size()); + for(Cluster<Model> clu : topc) { + buildHullsRecursively(clu, hier, hullmap); + } + + // This way, we draw each cluster only once. + // Unfortunately, not depth ordered (TODO!) + for(Cluster<Model> clu : clusters) { + DoubleObjPair<Polygon> pair = hullmap.get(clu), + mpair = hullmap.get(clu.getModel()); + // Plot the convex hull: + if(pair != null && pair.second != null && pair.second.size() > 1) { + SVGPath path = new SVGPath(pair.second); + // Approximate area (using bounding box) + double hullarea = SpatialUtil.volume(pair.second); + final double relativeArea = 1 - (hullarea / projarea); + final double relativeSize = pair.first / rel.size(); + final double corefact = (mpair == null) ? 1.0 : .5; + final double opacity = corefact * baseopacity * Math.sqrt(relativeSize * relativeArea); + addCSSClasses(svgp, cpol.getStyleForCluster(clu), opacity); + + Element hulls = path.makeElement(svgp); + SVGUtil.addCSSClass(hulls, CLUSTERHULL + cpol.getStyleForCluster(clu)); + layer.appendChild(hulls); + } + // For core density models, over-plot the core: + if(mpair != null && mpair.second != null && mpair.second.size() > 1) { + SVGPath path = new SVGPath(mpair.second); + // Approximate area (using bounding box) + double hullarea = SpatialUtil.volume(mpair.second); + final double relativeArea = 1 - (hullarea / projarea); + final double relativeSize = mpair.first / rel.size(); + final double opacity = .5 * baseopacity * Math.sqrt(relativeSize * relativeArea); + addCSSClasses(svgp, cpol.getStyleForCluster(clu), opacity); + + Element hulls = path.makeElement(svgp); + SVGUtil.addCSSClass(hulls, CLUSTERHULL + cpol.getStyleForCluster(clu)); + layer.appendChild(hulls); + } + } + } + else { + // Alpha shape mode. + // For alpha shapes we can't use the shortcut of convex hulls, + // but have to revisit all child clusters. + for(Cluster<Model> clu : clusters) { + ArrayList<Vector> ps = new ArrayList<>(); + double weight = addRecursively(ps, hier, clu); + List<Polygon> polys; + if(ps.size() < 1) { + continue; + } + if(ps.size() > 2) { + polys = (new AlphaShape(ps, settings.alpha * Projection.SCALE)).compute(); + } + else { + // Trivial polygon. Might still degenerate to a single point though. + polys = new ArrayList<>(1); + polys.add(new Polygon(ps)); + } + for(Polygon p : polys) { + SVGPath path = new SVGPath(p); + Element hulls = path.makeElement(svgp); + addCSSClasses(svgp, cpol.getStyleForCluster(clu), baseopacity * weight / rel.size()); + SVGUtil.addCSSClass(hulls, CLUSTERHULL + cpol.getStyleForCluster(clu)); + layer.appendChild(hulls); + } + } + } + } + + /** + * Recursively step through the clusters to build the hulls. + * + * @param clu Current cluster + * @param hier Clustering hierarchy + * @param hulls Hull map + */ + private DoubleObjPair<Polygon> buildHullsRecursively(Cluster<Model> clu, Hierarchy<Cluster<Model>> hier, Map<Object, DoubleObjPair<Polygon>> hulls) { + final Model model = clu.getModel(); + final DBIDs ids = clu.getIDs(); + boolean coremodel = false; + DBIDs cids = null; + if(model instanceof CoreObjectsModel) { + cids = ((CoreObjectsModel) model).getCoreObjects(); + coremodel = cids.size() > 0; + } + + GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D(); + GrahamScanConvexHull2D hull2 = coremodel ? new GrahamScanConvexHull2D() : null; + for(DBIDIter iter = ids.iter(); iter.valid(); iter.advance()) { + final double[] projv = proj.fastProjectDataToRenderSpace(rel.get(iter)); + if(projv[0] != projv[0] || projv[1] != projv[1]) { + continue; // NaN! + } + Vector projP = new Vector(projv); + hull.add(projP); + if(coremodel && cids.contains(iter)) { + hull2.add(projP); + } + } + double weight = ids.size(), cweight = coremodel ? cids.size() : 0.0; + if(hier != null && hulls != null) { + final int numc = hier.numChildren(clu); + if(numc > 0) { + for(Iter<Cluster<Model>> iter = hier.iterChildren(clu); iter.valid(); iter.advance()) { + final Cluster<Model> iclu = iter.get(); + DoubleObjPair<Polygon> poly = hulls.get(iclu); + if(poly == null) { + poly = buildHullsRecursively(iclu, hier, hulls); + } + // Add inner convex hull to outer convex hull. + for(ArrayListIter<Vector> vi = poly.second.iter(); vi.valid(); vi.advance()) { + hull.add(vi.get()); + } + // For a core model, include the inner core, too. + if(coremodel) { + DoubleObjPair<Polygon> ipoly = hulls.get(iclu.getModel()); + if(ipoly != null) { + for(ArrayListIter<Vector> vi = ipoly.second.iter(); vi.valid(); vi.advance()) { + hull2.add(vi.get()); + } + cweight += ipoly.first / numc; + } + } + weight += poly.first / numc; + } + } + } + DoubleObjPair<Polygon> pair = new DoubleObjPair<>(weight, hull.getHull()); + hulls.put(clu, pair); + if(coremodel) { + hulls.put(model, new DoubleObjPair<>(cweight, hull2.getHull())); + } + return pair; + } + + /** + * Recursively add a cluster and its children. + * + * @param hull Hull to add to + * @param hier Cluster hierarchy + * @param clus Current cluster + * @return Weight for visualization + */ + private double addRecursively(ArrayList<Vector> hull, Hierarchy<Cluster<Model>> hier, Cluster<Model> clus) { + final DBIDs ids = clus.getIDs(); + double weight = ids.size(); + for(DBIDIter iter = ids.iter(); iter.valid(); iter.advance()) { + double[] projP = proj.fastProjectDataToRenderSpace(rel.get(iter)); + if(projP[0] != projP[0] || projP[1] != projP[1]) { + continue; // NaN! + } + hull.add(new Vector(projP)); + } + for(Iter<Cluster<Model>> iter = hier.iterChildren(clus); iter.valid(); iter.advance()) { + weight += .5 * addRecursively(hull, hier, iter.get()); + } + return weight; + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp, int clusterID, double opac) { + final StyleLibrary style = context.getStyleLibrary(); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + CSSClass cls = new CSSClass(this, CLUSTERHULL + clusterID); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, .5 * style.getLineWidth(StyleLibrary.PLOT)); + + final String color = colors.getColor(clusterID); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, opac); + + svgp.addCSSClassOrLogError(cls); + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Alpha-Value for alpha-shapes + * + * <p> + * Key: {@code -hull.alpha} + * </p> + */ + public static final OptionID ALPHA_ID = new OptionID("hull.alpha", "Alpha value for hull drawing (in projected space!)."); + + /** + * Alpha value + */ + double alpha = Double.POSITIVE_INFINITY; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + DoubleParameter alphaP = new DoubleParameter(ALPHA_ID, Double.POSITIVE_INFINITY); + if(config.grab(alphaP)) { + alpha = alphaP.doubleValue(); + } + } + + @Override + protected ClusterHullVisualization makeInstance() { + return new ClusterHullVisualization(this); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterMeanVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterMeanVisualization.java new file mode 100644 index 00000000..bd597111 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterMeanVisualization.java @@ -0,0 +1,207 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Iterator; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.model.MeanModel; +import de.lmu.ifi.dbs.elki.data.model.MedoidModel; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.database.ids.DBID; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize the mean of a KMeans-Clustering + * + * @author Heidi Kolb + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class ClusterMeanVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Cluster Means"; + + /** + * Constructor. + */ + public ClusterMeanVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(final VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), ClusterMeanVisualization.this); + task.level = VisualizationTask.LEVEL_DATA + 1; + task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Heidi Kolb + * + * @apiviz.has MeanModel oneway - - visualizes + * @apiviz.has MedoidModel oneway - - visualizes + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * CSS class name for center of the means + */ + private static final String CSS_MEAN_CENTER = "mean-center"; + + /** + * CSS class name for center of the means + */ + private static final String CSS_MEAN = "mean-marker"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StylingPolicy spol = context.getStylingPolicy(); + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering(); + if(clustering.getAllClusters().size() == 0) { + return; + } + + StyleLibrary slib = context.getStyleLibrary(); + MarkerLibrary ml = slib.markers(); + double marker_size = slib.getSize(StyleLibrary.MARKERPLOT); + + // Small crosses for mean: + if(!svgp.getCSSClassManager().contains(CSS_MEAN_CENTER)) { + CSSClass center = new CSSClass(this, CSS_MEAN_CENTER); + center.setStatement(SVGConstants.CSS_STROKE_PROPERTY, slib.getTextColor(StyleLibrary.DEFAULT)); + center.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, slib.getLineWidth(StyleLibrary.AXIS_TICK) * .5); + svgp.addCSSClassOrLogError(center); + } + // Markers for the mean: + if(!svgp.getCSSClassManager().contains(CSS_MEAN)) { + CSSClass center = new CSSClass(this, CSS_MEAN); + center.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.7"); + svgp.addCSSClassOrLogError(center); + } + + Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator(); + for(int cnum = 0; ci.hasNext(); cnum++) { + Cluster<Model> clus = ci.next(); + Model model = clus.getModel(); + double[] mean = null; + try { + if(model instanceof MeanModel) { + final Vector mmean = ((MeanModel) model).getMean(); + if(mmean == null) { + continue; + } + mean = proj.fastProjectDataToRenderSpace(mmean); + } + else if(model instanceof MedoidModel) { + DBID medoid = ((MedoidModel) model).getMedoid(); + if(medoid == null) { + continue; + } + NumberVector v = rel.get(medoid); + if(v == null) { + continue; + } + mean = proj.fastProjectDataToRenderSpace(v); + } + else { + continue; + } + } + catch(ObjectNotFoundException e) { + continue; // Element not found. + } + + // add a greater Marker for the mean + Element meanMarker = ml.useMarker(svgp, layer, mean[0], mean[1], cnum, marker_size * 3); + SVGUtil.setAtt(meanMarker, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_MEAN); + + // Add a fine cross to mark the exact location of the mean. + Element meanMarkerCenter = svgp.svgLine(mean[0] - .7, mean[1], mean[0] + .7, mean[1]); + SVGUtil.setAtt(meanMarkerCenter, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_MEAN_CENTER); + Element meanMarkerCenter2 = svgp.svgLine(mean[0], mean[1] - .7, mean[0], mean[1] + .7); + SVGUtil.setAtt(meanMarkerCenter2, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_MEAN_CENTER); + + layer.appendChild(meanMarkerCenter); + layer.appendChild(meanMarkerCenter2); + } + svgp.updateStyleElement(); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterOrderVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterOrderVisualization.java new file mode 100644 index 00000000..9a029c79 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterOrderVisualization.java @@ -0,0 +1,162 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDVar; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Cluster order visualizer: connect objects via the spanning tree the cluster + * order represents. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +// TODO: draw sample only? +public class ClusterOrderVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Predecessor Graph"; + + /** + * Constructor. + */ + public ClusterOrderVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNewSiblings(context, start, ClusterOrder.class, ScatterPlotProjector.class, // + new VisualizationTree.Handler2<ClusterOrder, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, ClusterOrder co, ScatterPlotProjector<?> p) { + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + final VisualizationTask task = new VisualizationTask(NAME, context, co, rel, ClusterOrderVisualization.this); + task.initDefaultVisibility(false); + task.level = VisualizationTask.LEVEL_DATA - 1; + task.addUpdateFlags(VisualizationTask.ON_DATA); + context.addVis(co, task); + context.addVis(p, task); + } + }); + } + + /** + * Instance + * + * @author Erich Schubert + * + * @apiviz.has ClusterOrder oneway - - visualizes + */ + // TODO: listen for CLUSTER ORDER changes. + public class Instance extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * CSS class name + */ + private static final String CSSNAME = "predecessor"; + + /** + * The result we visualize + */ + protected ClusterOrder result; + + /** + * Constructor. + * + * @param task Visualization task. + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + result = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + CSSClass cls = new CSSClass(this, CSSNAME); + style.lines().formatCSSClass(cls, 0, style.getLineWidth(StyleLibrary.CLUSTERORDER)); + + svgp.addCSSClassOrLogError(cls); + + DBIDVar prev = DBIDUtil.newVar(); + for(DBIDIter it = result.iter(); it.valid(); it.advance()) { + result.getPredecessor(it, prev); + if(prev.isEmpty()) { + continue; + } + double[] thisVec = proj.fastProjectDataToRenderSpace(rel.get(it)); + double[] prevVec = proj.fastProjectDataToRenderSpace(rel.get(prev)); + + if(thisVec[0] != thisVec[0] || thisVec[1] != thisVec[1]) { + continue; // NaN! + } + if(prevVec[0] != prevVec[0] || prevVec[1] != prevVec[1]) { + continue; // NaN! + } + // FIXME: add arrow decorations! + Element arrow = svgp.svgLine(prevVec[0], prevVec[1], thisVec[0], thisVec[1]); + SVGUtil.setCSSClass(arrow, cls.getName()); + + layer.appendChild(arrow); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterStarVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterStarVisualization.java new file mode 100644 index 00000000..c0bedc4a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterStarVisualization.java @@ -0,0 +1,182 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Iterator; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.model.MeanModel; +import de.lmu.ifi.dbs.elki.data.model.MedoidModel; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize the mean of a KMeans-Clustering using stars. + * + * @author Heidi Kolb + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class ClusterStarVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Cluster Stars"; + + /** + * Constructor. + */ + public ClusterStarVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(final VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, ClusterStarVisualization.this); + task.level = VisualizationTask.LEVEL_DATA + 1; + task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY); + task.initDefaultVisibility(false); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Heidi Kolb + * + * @apiviz.has MeanModel oneway - - visualizes + * @apiviz.has MedoidModel oneway - - visualizes + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * CSS class name for center of the means + */ + private static final String CSS_MEAN_STAR = "mean-star"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StylingPolicy spol = context.getStylingPolicy(); + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering(); + if(clustering.getAllClusters().size() == 0) { + return; + } + + StyleLibrary slib = context.getStyleLibrary(); + ColorLibrary colors = slib.getColorSet(StyleLibrary.PLOT); + + Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator(); + for(int cnum = 0; ci.hasNext(); cnum++) { + Cluster<Model> clus = ci.next(); + Model model = clus.getModel(); + double[] mean; + if(model instanceof MeanModel) { + MeanModel mmodel = (MeanModel) model; + mean = proj.fastProjectDataToRenderSpace(mmodel.getMean()); + } + else if(model instanceof MedoidModel) { + MedoidModel mmodel = (MedoidModel) model; + mean = proj.fastProjectDataToRenderSpace(rel.get(mmodel.getMedoid())); + } + else { + continue; + } + + if(!svgp.getCSSClassManager().contains(CSS_MEAN_STAR + "_" + cnum)) { + CSSClass center = new CSSClass(this, CSS_MEAN_STAR + "_" + cnum); + center.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(cnum)); + center.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, slib.getLineWidth(StyleLibrary.PLOT)); + center.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.7"); + svgp.addCSSClassOrLogError(center); + } + + SVGPath star = new SVGPath(); + for(DBIDIter id = clus.getIDs().iter(); id.valid(); id.advance()) { + double[] obj = proj.fastProjectDataToRenderSpace(rel.get(id)); + star.moveTo(obj); + star.drawTo(mean); + } + Element stare = star.makeElement(svgp); + SVGUtil.setCSSClass(stare, CSS_MEAN_STAR + "_" + cnum); + layer.appendChild(stare); + } + svgp.updateStyleElement(); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/EMClusterVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/EMClusterVisualization.java new file mode 100644 index 00000000..04a2d241 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/EMClusterVisualization.java @@ -0,0 +1,487 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.model.EMModel; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.data.spatial.Polygon; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import de.lmu.ifi.dbs.elki.math.geometry.GrahamScanConvexHull2D; +import de.lmu.ifi.dbs.elki.math.linearalgebra.EigenPair; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Matrix; +import de.lmu.ifi.dbs.elki.math.linearalgebra.SortedEigenPairs; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.math.linearalgebra.pca.PCARunner; +import de.lmu.ifi.dbs.elki.utilities.ClassGenericsUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.EmptyParameterization; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperSphere; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualizer for generating SVG-Elements containing ellipses for first, second + * and third standard deviation. In more than 2-dimensional data, the class + * tries to approximate the cluster extends. + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class EMClusterVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "EM Cluster Models"; + + /** + * Constants for quantiles of standard deviation + */ + final static double[] sigma = new double[] { 0.41, 0.223, 0.047 }; + + /** + * Constructor + */ + public EMClusterVisualization() { + super(); + } + + @Override + public Instance makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), EMClusterVisualization.this); + task.level = VisualizationTask.LEVEL_DATA + 3; + task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Robert Rödler + * + * @apiviz.has EMModel oneway - - visualizes + * @apiviz.uses GrahamScanConvexHull2D + */ + // TODO: nicer stacking of n-fold hulls + // TODO: can we find a proper sphere for 3+ dimensions? + public class Instance extends AbstractScatterplotVisualization { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String EMBORDER = "EMClusterBorder"; + + /** + * Kappa constant, + */ + private static final double KAPPA = SVGHyperSphere.EUCLIDEAN_KAPPA; + + /** + * StyleParameter: + */ + private int times = 3; + + private int opacStyle = 1; + + private int softBorder = 1; + + private int drawStyle = 0; + + /** + * Constructor + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StylingPolicy spol = context.getStylingPolicy(); + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering(); + List<Cluster<Model>> clusters = clustering.getAllClusters(); + if(clusters.size() <= 1) { + return; + } + + StyleLibrary style = context.getStyleLibrary(); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + // PCARunner + PCARunner pcarun = ClassGenericsUtil.parameterizeOrAbort(PCARunner.class, new EmptyParameterization()); + + Iterator<Cluster<Model>> ci = clusters.iterator(); + for(int cnum = 0; cnum < clusters.size(); cnum++) { + Cluster<Model> clus = ci.next(); + DBIDs ids = clus.getIDs(); + if(ids.size() <= 0) { + continue; + } + if(!(clus.getModel() instanceof EMModel)) { + continue; + } + EMModel model = (EMModel) clus.getModel(); + + // Add cluster style + final String sname = EMBORDER + "_" + cnum; + if(!svgp.getCSSClassManager().contains(sname)) { + CSSClass cls = new CSSClass(this, sname); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5); + + String color = colors.getColor(cnum); + if(softBorder == 0) { + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color); + } + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.15); + + svgp.addCSSClassOrLogError(cls); + } + + Matrix covmat = model.getCovarianceMatrix(); + Vector centroid = model.getMean(); + Vector cent = new Vector(proj.fastProjectDataToRenderSpace(centroid)); + + // Compute the eigenvectors + SortedEigenPairs eps = pcarun.processCovarMatrix(covmat).getEigenPairs(); + Vector[] pc = new Vector[eps.size()]; + for(int i = 0; i < eps.size(); i++) { + EigenPair ep = eps.getEigenPair(i); + Vector sev = ep.getEigenvector().times(Math.sqrt(ep.getEigenvalue())); + pc[i] = new Vector(proj.fastProjectRelativeDataToRenderSpace(sev.getArrayRef())); + } + if(drawStyle != 0 || eps.size() == 2) { + drawSphere2D(sname, cent, pc); + } + else { + Polygon chres = makeHullComplex(pc); + drawHullLines(sname, cent, chres); + } + } + } + + /** + * Draw by approximating a sphere via cubic splines + * + * @param sname CSS class name + * @param cent center + * @param pc Principal components + */ + protected void drawSphere2D(String sname, Vector cent, Vector[] pc) { + CSSClass cls = opacStyle == 1 ? new CSSClass(null, "temp") : null; + for(int dim1 = 0; dim1 < pc.length - 1; dim1++) { + for(int dim2 = dim1 + 1; dim2 < pc.length; dim2++) { + for(int i = 1; i <= times; i++) { + SVGPath path = new SVGPath(); + + Vector p1 = cent.plusTimes(pc[dim1], i); + Vector p2 = cent.plusTimes(pc[dim2], i); + Vector p3 = cent.minusTimes(pc[dim1], i); + Vector p4 = cent.minusTimes(pc[dim2], i); + + path.moveTo(p1); + path.cubicTo(// + p1.plusTimes(pc[dim2], KAPPA * i), // + p2.plusTimes(pc[dim1], KAPPA * i), // + p2); + path.cubicTo(// + p2.minusTimes(pc[dim1], KAPPA * i), // + p3.plusTimes(pc[dim2], KAPPA * i), // + p3); + path.cubicTo(// + p3.minusTimes(pc[dim2], KAPPA * i), // + p4.minusTimes(pc[dim1], KAPPA * i), // + p4); + path.cubicTo(// + p4.plusTimes(pc[dim1], KAPPA * i), // + p1.minusTimes(pc[dim2], KAPPA * i), // + p1); + path.close(); + + Element ellipse = path.makeElement(svgp); + SVGUtil.addCSSClass(ellipse, sname); + if(cls != null) { + double s = (i >= 1 && i <= sigma.length) ? sigma[i - 1] : 0.0; + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, s); + SVGUtil.setAtt(ellipse, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS()); + } + layer.appendChild(ellipse); + } + } + } + } + + /** + * Approximate by convex hull. + * + * @param sname CSS name + * @param cent center + * @param chres Polygon around center + */ + protected void drawHullLines(String sname, Vector cent, Polygon chres) { + if(chres.size() <= 1) { + return; + } + CSSClass cls = opacStyle == 1 ? new CSSClass(null, "temp") : null; + for(int i = 1; i <= times; i++) { + SVGPath path = new SVGPath(); + for(int p = 0; p < chres.size(); p++) { + path.drawTo(cent.plusTimes(chres.get(p), i)); + } + path.close(); + Element ellipse = path.makeElement(svgp); + SVGUtil.addCSSClass(ellipse, sname); + if(cls != null) { + double s = (i >= 1 && i <= sigma.length) ? sigma[i - 1] : 0.0; + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, s); + SVGUtil.setAtt(ellipse, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS()); + } + layer.appendChild(ellipse); + } + } + + /** + * Build a convex hull to approximate the sphere. + * + * @param pc Principal components + * @return Polygon + */ + protected Polygon makeHull(Vector[] pc) { + GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D(); + + Vector diag = new Vector(0, 0); + for(int j = 0; j < pc.length; j++) { + hull.add(pc[j]); + hull.add(pc[j].times(-1)); + for(int k = j + 1; k < pc.length; k++) { + Vector q = pc[k]; + Vector ppq = pc[j].plus(q).timesEquals(MathUtil.SQRTHALF); + Vector pmq = pc[j].minus(q).timesEquals(MathUtil.SQRTHALF); + hull.add(ppq); + hull.add(ppq.times(-1)); + hull.add(pmq); + hull.add(pmq.times(-1)); + } + diag.plusEquals(pc[j]); + } + diag.timesEquals(1.0 / Math.sqrt(pc.length)); + hull.add(diag); + hull.add(diag.times(-1)); + + return hull.getHull(); + } + + /** + * Build a convex hull to approximate the sphere. + * + * @param pc Principal components + * @return Polygon + */ + protected Polygon makeHullComplex(Vector[] pc) { + GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D(); + + Vector diag = new Vector(0, 0); + for(int j = 0; j < pc.length; j++) { + hull.add(pc[j]); + hull.add(pc[j].times(-1)); + for(int k = j + 1; k < pc.length; k++) { + Vector q = pc[k]; + Vector ppq = pc[j].plus(q).timesEquals(MathUtil.SQRTHALF); + Vector pmq = pc[j].minus(q).timesEquals(MathUtil.SQRTHALF); + hull.add(ppq); + hull.add(ppq.times(-1)); + hull.add(pmq); + hull.add(pmq.times(-1)); + for(int l = k + 1; l < pc.length; l++) { + Vector r = pc[k]; + Vector ppqpr = ppq.plus(r).timesEquals(Math.sqrt(1 / 3.)); + Vector pmqpr = pmq.plus(r).timesEquals(Math.sqrt(1 / 3.)); + Vector ppqmr = ppq.minus(r).timesEquals(Math.sqrt(1 / 3.)); + Vector pmqmr = pmq.minus(r).timesEquals(Math.sqrt(1 / 3.)); + hull.add(ppqpr); + hull.add(ppqpr.times(-1)); + hull.add(pmqpr); + hull.add(pmqpr.times(-1)); + hull.add(ppqmr); + hull.add(ppqmr.times(-1)); + hull.add(pmqmr); + hull.add(pmqmr.times(-1)); + } + } + diag.plusEquals(pc[j]); + } + diag.timesEquals(1.0 / Math.sqrt(pc.length)); + hull.add(diag); + hull.add(diag.times(-1)); + return hull.getHull(); + } + + /** + * Approximate the hull using arcs. + * + * @param sname CSS name + * @param cent Center + * @param chres Polygon + */ + protected void drawHullArc(String sname, Vector cent, Polygon chres) { + if(chres.size() <= 1) { + return; + } + CSSClass cls = opacStyle == 1 ? new CSSClass(null, "temp") : null; + for(int i = 1; i <= times; i++) { + SVGPath path = new SVGPath(); + + ArrayList<Vector> delta = new ArrayList<>(chres.size()); + for(int p = 0; p < chres.size(); p++) { + Vector prev = chres.get((p - 1 + chres.size()) % chres.size()); + Vector curr = chres.get(p); + Vector next = chres.get((p + 1) % chres.size()); + Vector d1 = next.minus(curr).normalize(); + Vector d2 = curr.minus(prev).normalize(); + delta.add(d1.plus(d2)); + // delta.add(next.minus(prev)); + } + + for(int p = 0; p < chres.size(); p++) { + Vector cur = cent.plus(chres.get(p)); + Vector nex = cent.plus(chres.get((p + 1) % chres.size())); + Vector dcur = delta.get(p); + Vector dnex = delta.get((p + 1) % chres.size()); + drawArc(path, cent, cur, nex, dcur, dnex, i); + } + path.close(); + + Element ellipse = path.makeElement(svgp); + + SVGUtil.addCSSClass(ellipse, sname); + if(cls != null) { + double s = (i >= 1 && i <= sigma.length) ? sigma[i - 1] : 0.0; + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, s); + SVGUtil.setAtt(ellipse, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS()); + } + layer.appendChild(ellipse); + } + } + + /** + * Draw an arc to simulate the hyper ellipse. + * + * @param path Path to draw to + * @param cent Center + * @param pre Previous point + * @param nex Next point + * @param scale Scaling factor + */ + private void drawArc(SVGPath path, Vector cent, Vector pre, Vector nex, Vector oPrev, Vector oNext, double scale) { + // Delta vectors + final Vector rPrev = pre.minus(cent); + final Vector rNext = nex.minus(cent); + final Vector rPrNe = pre.minus(nex); + // Scaled fix points + final Vector sPrev = cent.plusTimes(rPrev, scale); + final Vector sNext = cent.plusTimes(rNext, scale); + // Orthogonal vectors to the relative vectors + // final Vector oPrev = new Vector(rPrev.get(1), -rPrev.get(0)); + // final Vector oNext = new Vector(-rNext.get(1), rNext.get(0)); + + // Compute the intersection of rPrev+tp*oPrev and rNext+tn*oNext + // rPrNe == rPrev - rNext + final double zp = rPrNe.get(0) * oNext.get(1) - rPrNe.get(1) * oNext.get(0); + final double zn = rPrNe.get(0) * oPrev.get(1) - rPrNe.get(1) * oPrev.get(0); + final double n = oPrev.get(1) * oNext.get(0) - oPrev.get(0) * oNext.get(1); + if(n == 0) { + LoggingUtil.warning("Parallel?!?"); + path.drawTo(sNext.get(0), sNext.get(1)); + return; + } + final double tp = Math.abs(zp / n); + final double tn = Math.abs(zn / n); + // LoggingUtil.warning("tp: "+tp+" tn: "+tn); + + // Guide points + final Vector gPrev = sPrev.plusTimes(oPrev, KAPPA * scale * tp); + final Vector gNext = sNext.minusTimes(oNext, KAPPA * scale * tn); + + if(!path.isStarted()) { + path.moveTo(sPrev); + } + // path.drawTo(sPrev); + // path.drawTo(gPrev); + // path.drawTo(gNext); + // path.drawTo(sNext)); + // path.moveTo(sPrev); + // if(tp < 0 || tn < 0) { + // path.drawTo(sNext); + // } + // else { + path.cubicTo(gPrev, gNext, sNext); + // } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/VoronoiVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/VoronoiVisualization.java new file mode 100644 index 00000000..f1d9051b --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/VoronoiVisualization.java @@ -0,0 +1,323 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.ArrayList; +import java.util.List; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.model.KMeansModel; +import de.lmu.ifi.dbs.elki.data.model.MedoidModel; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.database.ids.DBID; +import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D; +import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D.Triangle; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +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.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.EnumParameter; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.svg.VoronoiDraw; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualizer drawing Voronoi cells for k-means clusterings. + * + * See also: {@link de.lmu.ifi.dbs.elki.algorithm.clustering.kmeans.KMeansLloyd + * KMeans clustering} + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class VoronoiVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "k-means Voronoi cells"; + + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + private static final String KMEANSBORDER = "kmeans-border"; + + /** + * Visualization mode. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static enum Mode { + /** + * Draw Voronoi cells. + */ + VORONOI, // + /** + * Draw Delaunay triangulation. + */ + DELAUNAY, // + /** + * Draw both Delaunay and Voronoi. + */ + V_AND_D + } + + /** + * Visualization mode. + */ + private Mode mode; + + /** + * Constructor. + * + * @param mode Visualization mod + */ + public VoronoiVisualization(Mode mode) { + super(); + this.mode = mode; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), VoronoiVisualization.this); + task.level = VisualizationTask.LEVEL_DATA + 3; + task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Robert Rödler + * @author Erich Schubert + * + * @apiviz.has KMeansModel oneway - - visualizes + * @apiviz.has MedoidModel oneway - - visualizes + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * The Voronoi diagram. + */ + Element voronoi; + + /** + * Constructor. + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StylingPolicy spol = context.getStylingPolicy(); + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering(); + if(clustering.getAllClusters().size() <= 1) { + return; + } + + final int dim = proj.getInputDimensionality(); + if(dim != 2) { + return; + } + + addCSSClasses(svgp); + final List<Cluster<Model>> clusters = clustering.getAllClusters(); + + // Collect cluster means + ArrayList<Vector> vmeans = new ArrayList<>(clusters.size()); + ArrayList<double[]> means = new ArrayList<>(clusters.size()); + { + for(Cluster<Model> clus : clusters) { + Model model = clus.getModel(); + Vector mean; + try { + if(model instanceof KMeansModel) { + Vector mmean = ((KMeansModel) model).getMean(); + if(mmean == null) { + continue; + } + mean = mmean.getColumnVector(); + if(mean.getDimensionality() != dim) { + continue; + } + } + else if(model instanceof MedoidModel) { + DBID medoid = ((MedoidModel) model).getMedoid(); + if(medoid == null) { + continue; + } + NumberVector v = rel.get(medoid); + if(v == null) { + continue; + } + mean = v.getColumnVector(); + if(mean.getDimensionality() != dim) { + continue; + } + } + else { + continue; + } + } + catch(ObjectNotFoundException e) { + continue; // Element not found. + } + vmeans.add(mean); + means.add(mean.getArrayRef()); + } + } + + if(means.size() < 2) { + return; // Cannot visualize + } + if(means.size() == 2) { + if(mode == Mode.VORONOI || mode == Mode.V_AND_D) { + Element path = VoronoiDraw.drawFakeVoronoi(proj, means).makeElement(svgp); + SVGUtil.addCSSClass(path, KMEANSBORDER); + layer.appendChild(path); + } + if(mode == Mode.DELAUNAY || mode == Mode.V_AND_D) { + Element path = new SVGPath(proj.fastProjectDataToRenderSpace(means.get(0)))// + .drawTo(proj.fastProjectDataToRenderSpace(means.get(1))).makeElement(svgp); + SVGUtil.addCSSClass(path, KMEANSBORDER); + layer.appendChild(path); + } + } + else { + // Compute Delaunay Triangulation + ArrayList<Triangle> delaunay = new SweepHullDelaunay2D(vmeans).getDelaunay(); + if(mode == Mode.VORONOI || mode == Mode.V_AND_D) { + Element path = VoronoiDraw.drawVoronoi(proj, delaunay, means).makeElement(svgp); + SVGUtil.addCSSClass(path, KMEANSBORDER); + layer.appendChild(path); + } + if(mode == Mode.DELAUNAY || mode == Mode.V_AND_D) { + Element path = VoronoiDraw.drawDelaunay(proj, delaunay, means).makeElement(svgp); + SVGUtil.addCSSClass(path, KMEANSBORDER); + layer.appendChild(path); + } + } + } + + /** + * Adds the required CSS-Classes. + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + // Class for the distance markers + if(!svgp.getCSSClassManager().contains(KMEANSBORDER)) { + final StyleLibrary style = context.getStyleLibrary(); + CSSClass cls = new CSSClass(this, KMEANSBORDER); + cls = new CSSClass(this, KMEANSBORDER); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + svgp.addCSSClassOrLogError(cls); + } + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Mode for drawing: Voronoi, Delaunay, both. + * + * <p> + * Key: {@code -voronoi.mode} + * </p> + */ + public static final OptionID MODE_ID = new OptionID("voronoi.mode", "Mode for drawing the voronoi cells (and/or delaunay triangulation)"); + + /** + * Drawing mode. + */ + protected Mode mode; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + EnumParameter<Mode> modeP = new EnumParameter<>(MODE_ID, Mode.class, Mode.VORONOI); + if(config.grab(modeP)) { + mode = modeP.getValue(); + } + } + + @Override + protected VoronoiVisualization makeInstance() { + return new VoronoiVisualization(mode); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/package-info.java new file mode 100755 index 00000000..84f9f745 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for clustering results based on 2D projections.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/DensityEstimationOverlay.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/DensityEstimationOverlay.java new file mode 100644 index 00000000..95239509 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/DensityEstimationOverlay.java @@ -0,0 +1,254 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density; + +/* + 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 <http://www.gnu.org/licenses/>. + */ +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.Comparator; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import de.lmu.ifi.dbs.elki.math.MeanVariance; +import de.lmu.ifi.dbs.elki.result.KMLOutputHandler; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.documentation.Reference; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * A simple density estimation visualization, based on a simple kernel-density + * <em>in the projection, not the actual data!</em> + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +// TODO: Use sample only +public class DensityEstimationOverlay extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Density estimation overlay"; + + /** + * Constructor. + */ + public DensityEstimationOverlay() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, rel, p.getRelation(), DensityEstimationOverlay.this); + task.level = VisualizationTask.LEVEL_DATA + 1; + task.addUpdateFlags(VisualizationTask.ON_DATA); + task.initDefaultVisibility(false); + context.addVis(p, task); + } + } + + /** + * Instance for a particular data set. + * + * @author Erich Schubert + */ + // TODO: make parameterizable, in particular color map, kernel bandwidth and + // kernel function + public class Instance extends AbstractScatterplotVisualization { + /** + * Density map resolution + */ + private int resolution = 500; + + /** + * The actual image + */ + private BufferedImage img = null; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + if(img == null) { + renderImage(); + } + + CanvasSize canvas = proj.estimateViewport(); + String imguri = ThumbnailRegistryEntry.INTERNAL_PREFIX + ThumbnailRegistryEntry.registerImage(img); + Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG); + SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE); + SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, canvas.minx); + SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, canvas.miny); + SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, canvas.maxx - canvas.minx); + SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, canvas.maxy - canvas.miny); + SVGUtil.setAtt(itag, SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_OPACITY_PROPERTY + ": .5"); + itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, imguri); + + layer.appendChild(itag); + } + + @Reference(authors = "D. W. Scott", title = "Multivariate density estimation: Theory, Practice, and Visualization", // + booktitle = "Multivariate Density Estimation: Theory, Practice, and Visualization", // + url = "http://dx.doi.org/10.1002/9780470316849") + private double[] initializeBandwidth(double[][] data) { + MeanVariance mv0 = new MeanVariance(); + MeanVariance mv1 = new MeanVariance(); + // For Kernel bandwidth. + for(double[] projected : data) { + mv0.put(projected[0]); + mv1.put(projected[1]); + } + // Set bandwidths according to Scott's rule: + // Note: in projected space, d=2. + double[] bandwidth = new double[2]; + bandwidth[0] = MathUtil.SQRT5 * mv0.getSampleStddev() * Math.pow(rel.size(), -1 / 6.); + bandwidth[1] = MathUtil.SQRT5 * mv1.getSampleStddev() * Math.pow(rel.size(), -1 / 6.); + return bandwidth; + } + + private void renderImage() { + // TODO: SAMPLE? Do region queries? + // Project the data just once, keep a copy. + double[][] data = new double[rel.size()][]; + { + int i = 0; + for(DBIDIter iditer = rel.iterDBIDs(); iditer.valid(); iditer.advance()) { + data[i] = proj.fastProjectDataToRenderSpace(rel.get(iditer)); + i++; + } + } + double[] bandwidth = initializeBandwidth(data); + // Compare by first component + Comparator<double[]> comp0 = new Comparator<double[]>() { + @Override + public int compare(double[] o1, double[] o2) { + return Double.compare(o1[0], o2[0]); + } + }; + // Compare by second component + Comparator<double[]> comp1 = new Comparator<double[]>() { + @Override + public int compare(double[] o1, double[] o2) { + return Double.compare(o1[1], o2[1]); + } + }; + // TODO: choose comparator order based on smaller bandwidth? + Arrays.sort(data, comp0); + + CanvasSize canvas = proj.estimateViewport(); + double min0 = canvas.minx, max0 = canvas.maxx, + ste0 = (max0 - min0) / resolution; + double min1 = canvas.miny, max1 = canvas.maxy, + ste1 = (max1 - min1) / resolution; + + double kernf = 9. / (16 * bandwidth[0] * bandwidth[1]); + double maxdens = 0.0; + double[][] dens = new double[resolution][resolution]; + { + // TODO: incrementally update the loff/roff values? + for(int x = 0; x < resolution; x++) { + double xlow = min0 + ste0 * x, xhig = xlow + ste0; + int loff = unflip(Arrays.binarySearch(data, new double[] { xlow - bandwidth[0] }, comp0)); + int roff = unflip(Arrays.binarySearch(data, new double[] { xhig + bandwidth[0] }, comp0)); + // Resort by second component + Arrays.sort(data, loff, roff, comp1); + for(int y = 0; y < resolution; y++) { + double ylow = min1 + ste1 * y, yhig = ylow + ste1; + int boff = unflip(Arrays.binarySearch(data, loff, roff, new double[] { 0, ylow - bandwidth[1] }, comp1)); + int toff = unflip(Arrays.binarySearch(data, loff, roff, new double[] { 0, yhig + bandwidth[1] }, comp1)); + for(int pos = boff; pos < toff; pos++) { + double[] val = data[pos]; + double d0 = (val[0] < xlow) ? (xlow - val[0]) : (val[0] > xhig) ? (val[0] - xhig) : 0; + double d1 = (val[1] < ylow) ? (ylow - val[1]) : (val[1] > yhig) ? (val[1] - yhig) : 0; + d0 = d0 / bandwidth[0]; + d1 = d1 / bandwidth[1]; + dens[x][y] += kernf * (1 - d0 * d0) * (1 - d1 * d1); + } + maxdens = Math.max(maxdens, dens[x][y]); + } + // Restore original sorting, as the intervals overlap + Arrays.sort(data, loff, roff, comp0); + } + } + img = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_ARGB); + { + for(int x = 0; x < resolution; x++) { + for(int y = 0; y < resolution; y++) { + int rgb = KMLOutputHandler.getColorForValue(dens[x][y] / maxdens).getRGB(); + img.setRGB(x, y, rgb); + } + } + } + } + + private int unflip(int binarySearch) { + if(binarySearch < 0) { + return (-binarySearch) - 1; + } + else { + return binarySearch; + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/package-info.java new file mode 100755 index 00000000..ab8cc128 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for data set density in a scatterplot projection.</p> + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeMBRVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeMBRVisualization.java new file mode 100644 index 00000000..67fd97a7 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeMBRVisualization.java @@ -0,0 +1,252 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.index.tree.spatial.SpatialEntry; +import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTree; +import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTreeNode; +import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.rstar.RStarTreeNode; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.BitsUtil; +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.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperCube; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize the bounding rectangles of an R-Tree based index. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class TreeMBRVisualization extends AbstractVisFactory { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String INDEX = "index"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Index MBRs"; + + /** + * Settings + */ + protected Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public TreeMBRVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance<RStarTreeNode, SpatialEntry>(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNewSiblings(context, start, AbstractRStarTree.class, ScatterPlotProjector.class, // + new VisualizationTree.Handler2<AbstractRStarTree<RStarTreeNode, SpatialEntry, ?>, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, AbstractRStarTree<RStarTreeNode, SpatialEntry, ?> tree, ScatterPlotProjector<?> p) { + final VisualizationTask task = new VisualizationTask(NAME, context, (Result) tree, p.getRelation(), TreeMBRVisualization.this); + task.level = VisualizationTask.LEVEL_BACKGROUND + 1; + task.initDefaultVisibility(false); + context.addVis((Result) tree, task); + context.addVis(p, task); + } + }); + } + + /** + * Instance for a particular tree + * + * @author Erich Schubert + * + * @apiviz.has AbstractRStarTree oneway - - visualizes + * @apiviz.uses SVGHyperCube + * + * @param <N> Tree node type + * @param <E> Tree entry type + */ + // TODO: listen for tree changes instead of data changes? + public class Instance<N extends AbstractRStarTreeNode<N, E>, E extends SpatialEntry> extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * The tree we visualize + */ + protected AbstractRStarTree<N, E, ?> tree; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + @SuppressWarnings("unchecked") + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.tree = AbstractRStarTree.class.cast(task.getResult()); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + int projdim = BitsUtil.cardinality(proj.getVisibleDimensions2D()); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + if(tree != null) { + E root = tree.getRootEntry(); + for(int i = 0; i < tree.getHeight(); i++) { + CSSClass cls = new CSSClass(this, INDEX + i); + // Relative depth of this level. 1.0 = toplevel + final double relDepth = 1. - (((double) i) / tree.getHeight()); + if(settings.fill) { + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.1 / (projdim - 1)); + } + else { + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + } + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + svgp.addCSSClassOrLogError(cls); + } + visualizeRTreeEntry(svgp, layer, proj, tree, root, 0); + } + } + + /** + * Recursively draw the MBR rectangles. + * + * @param svgp SVG Plot + * @param layer Layer + * @param proj Projection + * @param rtree Rtree to visualize + * @param entry Current entry + * @param depth Current depth + */ + private void visualizeRTreeEntry(SVGPlot svgp, Element layer, Projection2D proj, AbstractRStarTree<? extends N, E, ?> rtree, E entry, int depth) { + SpatialComparable mbr = entry; + + if(settings.fill) { + Element r = SVGHyperCube.drawFilled(svgp, INDEX + depth, proj, mbr); + layer.appendChild(r); + } + else { + Element r = SVGHyperCube.drawFrame(svgp, proj, mbr); + SVGUtil.setCSSClass(r, INDEX + depth); + layer.appendChild(r); + } + + if(!entry.isLeafEntry()) { + N node = rtree.getNode(entry); + for(int i = 0; i < node.getNumEntries(); i++) { + E child = node.getEntry(i); + if(!child.isLeafEntry()) { + visualizeRTreeEntry(svgp, layer, proj, rtree, child, depth + 1); + } + } + } + } + + @Override + public void destroy() { + super.destroy(); + context.removeDataStoreListener(this); + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Flag for half-transparent filling of bubbles. + * + * <p> + * Key: {@code -index.fill} + * </p> + */ + public static final OptionID FILL_ID = new OptionID("index.fill", "Partially transparent filling of index pages."); + + protected boolean fill = false; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + Flag fillF = new Flag(FILL_ID); + if(config.grab(fillF)) { + fill = fillF.isTrue(); + } + } + + @Override + protected TreeMBRVisualization makeInstance() { + return new TreeMBRVisualization(this); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeSphereVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeSphereVisualization.java new file mode 100644 index 00000000..f4eb6a95 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeSphereVisualization.java @@ -0,0 +1,321 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBID; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.distance.distancefunction.DistanceFunction; +import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.EuclideanDistanceFunction; +import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.LPNormDistanceFunction; +import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.ManhattanDistanceFunction; +import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.AbstractMTree; +import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.AbstractMTreeNode; +import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.MTreeEntry; +import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.mtree.MTreeNode; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.BitsUtil; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperSphere; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize the bounding sphere of a metric index. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class TreeSphereVisualization extends AbstractVisFactory { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String INDEX = "index"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Index Spheres"; + + /** + * Drawing modes. + * + * @apiviz.exclude + */ + private enum Modus { + MANHATTAN, EUCLIDEAN, LPCROSS + } + + /** + * Settings + */ + protected Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public TreeSphereVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNewSiblings(context, start, AbstractMTree.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<AbstractMTree<?, ?, ?, ?>, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, AbstractMTree<?, ?, ?, ?> tree, ScatterPlotProjector<?> p) { + Relation<?> rel = p.getRelation(); + if(!canVisualize(rel, tree)) { + return; + } + final VisualizationTask task = new VisualizationTask(NAME, context, tree, rel, TreeSphereVisualization.this); + task.level = VisualizationTask.LEVEL_BACKGROUND + 1; + task.initDefaultVisibility(false); + context.addVis((Result) tree, task); + context.addVis(p, task); + } + }); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance<MTreeNode<Object>, MTreeEntry>(task, plot, width, height, proj); + } + + /** + * Get the "p" value of an Lp norm. + * + * @param tree Tree to visualize + * @return p value + */ + public static double getLPNormP(AbstractMTree<?, ?, ?, ?> tree) { + // Note: we deliberately lose generics here, so the compilers complain + // less on the next typecheck and cast! + DistanceFunction<?> distanceFunction = tree.getDistanceFunction(); + if(LPNormDistanceFunction.class.isInstance(distanceFunction)) { + return ((LPNormDistanceFunction) distanceFunction).getP(); + } + return 0; + } + + /** + * Test for a visualizable index in the context's database. + * + * @param rel Vector relation + * @param tree Tree to visualize + * @return whether the tree is visualizable + */ + public static boolean canVisualize(Relation<?> rel, AbstractMTree<?, ?, ?, ?> tree) { + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return false; + } + return getLPNormP(tree) > 0; + } + + /** + * Instance for a particular tree. + * + * @author Erich Schubert + * + * @apiviz.has AbstractMTree oneway - - visualizes + * @apiviz.uses SVGHyperSphere + * + * @param <N> Tree node type + * @param <E> Tree entry type + */ + // TODO: listen for tree changes! + public class Instance<N extends AbstractMTreeNode<?, N, E>, E extends MTreeEntry> extends AbstractScatterplotVisualization implements DataStoreListener { + protected double p; + + /** + * Drawing mode (distance) to use + */ + protected Modus dist = Modus.LPCROSS; + + /** + * The tree we visualize + */ + protected AbstractMTree<?, N, E, ?> tree; + + /** + * Constructor + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + @SuppressWarnings("unchecked") + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.tree = AbstractMTree.class.cast(task.getResult()); + this.p = getLPNormP(this.tree); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + int projdim = BitsUtil.cardinality(proj.getVisibleDimensions2D()); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + p = getLPNormP(tree); + if(tree != null) { + if(ManhattanDistanceFunction.class.isInstance(tree.getDistanceFunction())) { + dist = Modus.MANHATTAN; + } + else if(EuclideanDistanceFunction.class.isInstance(tree.getDistanceFunction())) { + dist = Modus.EUCLIDEAN; + } + else { + dist = Modus.LPCROSS; + } + E root = tree.getRootEntry(); + final int mtheight = tree.getHeight(); + for(int i = 0; i < mtheight; i++) { + CSSClass cls = new CSSClass(this, INDEX + i); + // Relative depth of this level. 1.0 = toplevel + final double relDepth = 1. - (((double) i) / mtheight); + if(settings.fill) { + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.1 / (projdim - 1)); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + } + else { + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + } + svgp.addCSSClassOrLogError(cls); + } + visualizeMTreeEntry(svgp, this.layer, proj, tree, root, 0); + } + } + + /** + * Recursively draw the MBR rectangles. + * + * @param svgp SVG Plot + * @param layer Layer + * @param proj Projection + * @param mtree Mtree to visualize + * @param entry Current entry + * @param depth Current depth + */ + private void visualizeMTreeEntry(SVGPlot svgp, Element layer, Projection2D proj, AbstractMTree<?, N, E, ?> mtree, E entry, int depth) { + DBID roid = entry.getRoutingObjectID(); + if(roid != null) { + NumberVector ro = rel.get(roid); + double rad = entry.getCoveringRadius(); + + final Element r; + if(dist == Modus.MANHATTAN) { + r = SVGHyperSphere.drawManhattan(svgp, proj, ro, rad); + } + else if(dist == Modus.EUCLIDEAN) { + r = SVGHyperSphere.drawEuclidean(svgp, proj, ro, rad); + } + // TODO: add visualizer for infinity norm? + else { + // r = SVGHyperSphere.drawCross(svgp, proj, ro, rad); + r = SVGHyperSphere.drawLp(svgp, proj, ro, rad, p); + } + SVGUtil.setCSSClass(r, INDEX + (depth - 1)); + layer.appendChild(r); + } + + if(!entry.isLeafEntry()) { + N node = mtree.getNode(entry); + for(int i = 0; i < node.getNumEntries(); i++) { + E child = node.getEntry(i); + if(!child.isLeafEntry()) { + visualizeMTreeEntry(svgp, layer, proj, mtree, child, depth + 1); + } + } + } + } + + @Override + public void destroy() { + super.destroy(); + context.removeDataStoreListener(this); + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + protected boolean fill = false; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + Flag fillF = new Flag(TreeMBRVisualization.Parameterizer.FILL_ID); + if(config.grab(fillF)) { + fill = fillF.isTrue(); + } + } + + @Override + protected TreeSphereVisualization makeInstance() { + return new TreeSphereVisualization(this); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/package-info.java new file mode 100755 index 00000000..7da4da26 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for index structures based on 2D projections.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/BubbleVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/BubbleVisualization.java new file mode 100644 index 00000000..f99b52f4 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/BubbleVisualization.java @@ -0,0 +1,333 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDRef; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.outlier.OutlierResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.documentation.Reference; +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.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ObjectParameter; +import de.lmu.ifi.dbs.elki.utilities.scaling.ScalingFunction; +import de.lmu.ifi.dbs.elki.utilities.scaling.outlier.OutlierScalingFunction; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Generates a SVG-Element containing bubbles. A Bubble is a circle visualizing + * an outlierness-score, with its center at the position of the visualized + * object and its radius depending on the objects score. + * + * @author Remigius Wojdanowski + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +@Reference(authors = "E. Achtert, H.-P. Kriegel, L. Reichert, E. Schubert, R. Wojdanowski, A. Zimek", // +title = "Visual Evaluation of Outlier Detection Models", // +booktitle = "Proceedings of the 15th International Conference on Database Systems for Advanced Applications (DASFAA), Tsukuba, Japan, 2010", // +url = "http://dx.doi.org/10.1007/978-3-642-12098-5_34") +public class BubbleVisualization extends AbstractVisFactory { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc. + */ + public static final String BUBBLE = "bubble"; + + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Outlier Bubbles"; + + /** + * Current settings + */ + protected Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public BubbleVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + if(settings.scaling != null && settings.scaling instanceof OutlierScalingFunction) { + final OutlierResult outlierResult = task.getResult(); + ((OutlierScalingFunction) settings.scaling).prepare(outlierResult); + } + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNewSiblings(context, start, OutlierResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<OutlierResult, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, OutlierResult o, ScatterPlotProjector<?> p) { + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + boolean vis = true; + // Quick and dirty hack: hide if parent result is also an outlier result + // Since that probably is already visible and we're redundant. + for(Hierarchy.Iter<Result> r = o.getHierarchy().iterParents(o); r.valid(); r.advance()) { + if(r.get() instanceof OutlierResult) { + vis = false; + break; + } + } + final VisualizationTask task = new VisualizationTask(NAME, context, o, rel, BubbleVisualization.this); + task.level = VisualizationTask.LEVEL_DATA; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY); + task.initDefaultVisibility(vis); + context.addVis(o, task); + context.addVis(p, task); + } + }); + } + + /** + * Factory for producing bubble visualizations + * + * @author Erich Schubert + * + * @apiviz.has OutlierResult oneway - - visualizes + */ + public class Instance extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * The outlier result to visualize + */ + protected OutlierResult result; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.result = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + StyleLibrary style = context.getStyleLibrary(); + StylingPolicy stylepolicy = context.getStylingPolicy(); + // bubble size + final double bubble_size = style.getSize(StyleLibrary.BUBBLEPLOT); + if(stylepolicy instanceof ClassStylingPolicy) { + ClassStylingPolicy colors = (ClassStylingPolicy) stylepolicy; + setupCSS(svgp, colors); + // draw data + for(DBIDIter objId = sample.getSample().iter(); objId.valid(); objId.advance()) { + final double radius = getScaledForId(objId); + if(radius > 0.01 && !Double.isInfinite(radius)) { + final NumberVector vec = rel.get(objId); + if(vec != null) { + double[] v = proj.fastProjectDataToRenderSpace(vec); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + Element circle = svgp.svgCircle(v[0], v[1], radius * bubble_size); + SVGUtil.addCSSClass(circle, BUBBLE + colors.getStyleForDBID(objId)); + layer.appendChild(circle); + } + } + } + } + else { + // draw data + for(DBIDIter objId = sample.getSample().iter(); objId.valid(); objId.advance()) { + final double radius = getScaledForId(objId); + if(radius > 0.01 && !Double.isInfinite(radius)) { + final NumberVector vec = rel.get(objId); + if(vec != null) { + double[] v = proj.fastProjectDataToRenderSpace(vec); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + Element circle = svgp.svgCircle(v[0], v[1], radius * bubble_size); + int color = stylepolicy.getColorForDBID(objId); + final StringBuilder cssstyle = new StringBuilder(); + if(settings.fill) { + cssstyle.append(SVGConstants.CSS_FILL_PROPERTY).append(':').append(SVGUtil.colorToString(color)); + cssstyle.append(SVGConstants.CSS_FILL_OPACITY_PROPERTY).append(":0.5"); + } + else { + cssstyle.append(SVGConstants.CSS_STROKE_VALUE).append(':').append(SVGUtil.colorToString(color)); + cssstyle.append(SVGConstants.CSS_FILL_PROPERTY).append(':').append(SVGConstants.CSS_NONE_VALUE); + } + SVGUtil.setAtt(circle, SVGConstants.SVG_STYLE_ATTRIBUTE, cssstyle.toString()); + layer.appendChild(circle); + } + } + } + } + } + + /** + * Registers the Bubble-CSS-Class at a SVGPlot. + * + * @param svgp the SVGPlot to register the Tooltip-CSS-Class. + * @param policy Clustering to use + */ + private void setupCSS(SVGPlot svgp, ClassStylingPolicy policy) { + final StyleLibrary style = context.getStyleLibrary(); + ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + // creating IDs manually because cluster often return a null-ID. + for(int clusterID = policy.getMinStyle(); clusterID < policy.getMaxStyle(); clusterID++) { + CSSClass bubble = new CSSClass(svgp, BUBBLE + clusterID); + bubble.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); + + String color = colors.getColor(clusterID); + + if(settings.fill) { + bubble.setStatement(SVGConstants.CSS_FILL_PROPERTY, color); + bubble.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.5); + } + else { + // for diamond-shaped strokes, see bugs.sun.com, bug ID 6294396 + bubble.setStatement(SVGConstants.CSS_STROKE_VALUE, color); + bubble.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + } + + svgp.addCSSClassOrLogError(bubble); + } + } + + /** + * Convenience method to apply scalings in the right order. + * + * @param id object ID to get scaled score for + * @return a Double representing a outlierness-score, after it has modified + * by the given scales. + */ + protected double getScaledForId(DBIDRef id) { + double d = result.getScores().doubleValue(id); + if(Double.isNaN(d) || Double.isInfinite(d)) { + return 0.0; + } + if(settings.scaling == null) { + return result.getOutlierMeta().normalizeScore(d); + } + else { + return settings.scaling.getScaled(d); + } + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Flag for half-transparent filling of bubbles. + * + * <p> + * Key: {@code -bubble.fill} + * </p> + */ + public static final OptionID FILL_ID = new OptionID("bubble.fill", "Half-transparent filling of bubbles."); + + /** + * Parameter for scaling functions + * + * <p> + * Key: {@code -bubble.scaling} + * </p> + */ + public static final OptionID SCALING_ID = new OptionID("bubble.scaling", "Additional scaling function for bubbles."); + + /** + * Fill parameter. + */ + protected boolean fill; + + /** + * Scaling function to use for Bubbles + */ + protected ScalingFunction scaling; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + Flag fillF = new Flag(FILL_ID); + if(config.grab(fillF)) { + fill = fillF.isTrue(); + } + + ObjectParameter<ScalingFunction> scalingP = new ObjectParameter<>(SCALING_ID, OutlierScalingFunction.class, true); + if(config.grab(scalingP)) { + scaling = scalingP.instantiateClass(config); + } + } + + @Override + protected BubbleVisualization makeInstance() { + return new BubbleVisualization(this); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/COPVectorVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/COPVectorVisualization.java new file mode 100644 index 00000000..b2abeb9f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/COPVectorVisualization.java @@ -0,0 +1,189 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.algorithm.outlier.COP; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.result.outlier.OutlierResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.documentation.Reference; +import de.lmu.ifi.dbs.elki.utilities.documentation.Title; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize error vectors as produced by COP. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + * @apiviz.has OutlierResult oneway - - visualizes + */ +@Title("COP: Correlation Outlier Probability") +@Reference(authors = "Hans-Peter Kriegel, Peer Kröger, Erich Schubert, Arthur Zimek", // +title = "Outlier Detection in Arbitrarily Oriented Subspaces", // +booktitle = "Proc. IEEE International Conference on Data Mining (ICDM 2012)", // +url = "http://dx.doi.org/10.1109/ICDM.2012.21") +public class COPVectorVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "Error Vectors"; + + /** + * Constructor. + */ + public COPVectorVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNewSiblings(context, start, OutlierResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<OutlierResult, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, OutlierResult o, ScatterPlotProjector<?> p) { + final Relation<?> rel2 = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel2.getDataTypeInformation())) { + return; + } + Hierarchy.Iter<Relation<?>> it1 = VisualizationTree.filterResults(context, o, Relation.class); + for(; it1.valid(); it1.advance()) { + Relation<?> rel = it1.get(); + if(!rel.getShortName().equals(COP.COP_ERRORVEC)) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, rel, rel2, COPVectorVisualization.this); + task.level = VisualizationTask.LEVEL_DATA; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE); + context.addVis(o, task); + context.addVis(p, task); + } + } + }); + } + + /** + * Visualize error vectors as produced by COP. + * + * @author Erich Schubert + */ + public class Instance extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String VEC = "copvec"; + + /** + * The outlier result to visualize + */ + protected Relation<Vector> result; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.result = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + setupCSS(svgp); + for(DBIDIter objId = sample.getSample().iter(); objId.valid(); objId.advance()) { + Vector evec = result.get(objId); + if(evec == null) { + continue; + } + double[] ev = proj.fastProjectRelativeDataToRenderSpace(evec); + // TODO: avoid hard-coded plot threshold + if(VMath.euclideanLength(ev) < 0.01) { + continue; + } + final NumberVector vec = rel.get(objId); + if(vec == null) { + continue; + } + double[] v = proj.fastProjectDataToRenderSpace(vec); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + Element arrow = svgp.svgLine(v[0], v[1], v[0] + ev[0], v[1] + ev[1]); + SVGUtil.addCSSClass(arrow, VEC); + layer.appendChild(arrow); + } + } + + /** + * Registers the COP error vector-CSS-Class at a SVGPlot. + * + * @param svgp the SVGPlot to register the Tooltip-CSS-Class. + */ + private void setupCSS(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + CSSClass bubble = new CSSClass(svgp, VEC); + bubble.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) / 2); + + // ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + String color = "red"; // TODO: use style library + bubble.setStatement(SVGConstants.CSS_STROKE_VALUE, color); + bubble.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + svgp.addCSSClassOrLogError(bubble); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/package-info.java new file mode 100755 index 00000000..0800548e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for outlier scores based on 2D projections.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/package-info.java new file mode 100755 index 00000000..4f5ed7c4 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/package-info.java @@ -0,0 +1,33 @@ +/** + * <p>Visualizers based on scatterplots.</p> + * + * @apiviz.exclude de.lmu.ifi.dbs.elki.visualization.batikutil.* + * @apiviz.exclude de.lmu.ifi.dbs.elki.visualization.svg.* + * @apiviz.exclude de.lmu.ifi.dbs.elki.result.* + * @apiviz.exclude de.lmu.ifi.dbs.elki.index.* + * @apiviz.exclude de.lmu.ifi.dbs.elki.data.* + * @apiviz.exclude de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/DistanceFunctionVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/DistanceFunctionVisualization.java new file mode 100644 index 00000000..4751605f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/DistanceFunctionVisualization.java @@ -0,0 +1,368 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.VectorUtil; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.ids.DoubleDBIDListIter; +import de.lmu.ifi.dbs.elki.database.ids.DoubleDBIDPair; +import de.lmu.ifi.dbs.elki.database.ids.KNNList; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.distance.distancefunction.ArcCosineDistanceFunction; +import de.lmu.ifi.dbs.elki.distance.distancefunction.CosineDistanceFunction; +import de.lmu.ifi.dbs.elki.distance.distancefunction.DistanceFunction; +import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.LPNormDistanceFunction; +import de.lmu.ifi.dbs.elki.index.preprocessed.knn.AbstractMaterializeKNNPreprocessor; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.MathUtil; +import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperSphere; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Factory for visualizers to generate an SVG-Element containing dots as markers + * representing the kNN of the selected Database objects. + * + * To use this, add a kNN preprocessor index to your database! + * + * @author Erich Schubert + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +// FIXME: for >2 dimensions, cosine doesn't seem to be correct yet. +public class DistanceFunctionVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + public static final String NAME = "k Nearest Neighbor Visualization"; + + /** + * Constructor + */ + public DistanceFunctionVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + VisualizationTree.findNewSiblings(context, start, AbstractMaterializeKNNPreprocessor.class, ScatterPlotProjector.class, // + new VisualizationTree.Handler2<AbstractMaterializeKNNPreprocessor<?>, ScatterPlotProjector<?>>() { + @Override + public void process(VisualizerContext context, AbstractMaterializeKNNPreprocessor<?> kNN, ScatterPlotProjector<?> p) { + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + return; + } + final VisualizationTask task = new VisualizationTask(NAME, context, kNN, rel, DistanceFunctionVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 1; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_SELECTION); + context.addVis(kNN, task); + context.addVis(p, task); + } + }); + } + + /** + * Get the "p" value of an Lp norm. + * + * @param kNN kNN preprocessor + * @return p of LP norm, or NaN + */ + public static double getLPNormP(AbstractMaterializeKNNPreprocessor<?> kNN) { + DistanceFunction<?> distanceFunction = kNN.getDistanceQuery().getDistanceFunction(); + if(LPNormDistanceFunction.class.isInstance(distanceFunction)) { + return ((LPNormDistanceFunction) distanceFunction).getP(); + } + return Double.NaN; + } + + /** + * Test whether the given preprocessor used an angular distance function + * + * @param kNN kNN preprocessor + * @return true when angular + */ + public static boolean isAngularDistance(AbstractMaterializeKNNPreprocessor<?> kNN) { + DistanceFunction<?> distanceFunction = kNN.getDistanceQuery().getDistanceFunction(); + if(CosineDistanceFunction.class.isInstance(distanceFunction)) { + return true; + } + if(ArcCosineDistanceFunction.class.isInstance(distanceFunction)) { + return true; + } + return false; + } + + /** + * Visualizes Cosine and ArcCosine distance functions + * + * @param svgp SVG Plot + * @param proj Visualization projection + * @param mid mean vector + * @param angle Opening angle in radians + * @return path element + */ + public static Element drawCosine(SVGPlot svgp, Projection2D proj, NumberVector mid, double angle) { + // Project origin + double[] pointOfOrigin = proj.fastProjectDataToRenderSpace(new double[proj.getInputDimensionality()]); + + // direction of the selected Point + double[] selPoint = proj.fastProjectDataToRenderSpace(mid); + + double[] range1, range2; + { + // Rotation plane: + double[] p1 = proj.fastProjectRenderToDataSpace(selPoint[0] + 10, selPoint[1]); + double[] p2 = proj.fastProjectRenderToDataSpace(selPoint[0], selPoint[1] + 10); + double[] pm = mid.getColumnVector().getArrayRef(); + // Compute relative vectors + VMath.minusEquals(p1, pm); + VMath.minusEquals(p2, pm); + // Scale p1 and p2 to unit length: + VMath.timesEquals(p1, 1. / VMath.euclideanLength(p1)); + VMath.timesEquals(p2, 1. / VMath.euclideanLength(p2)); + { + double test = VMath.scalarProduct(p1, p2); + if(Math.abs(test) > 1E-10) { + LoggingUtil.warning("Projection does not seem to be orthogonal?"); + } + } + // Project onto p1, p2: + double l1 = VMath.scalarProduct(pm, p1), l2 = VMath.scalarProduct(pm, p2); + // Rotate projection by + and - angle + // Using sin(-x) = -sin(x) and cos(-x)=cos(x) + final double cangle = Math.cos(angle), + sangle = MathUtil.cosToSin(angle, cangle); + double r11 = +cangle * l1 - sangle * l2, r12 = +sangle * l1 + cangle * l2; + double r21 = +cangle * l1 + sangle * l2, r22 = -sangle * l1 + cangle * l2; + // Build rotated vectors - remove projected component, add rotated + // component: + double[] r1 = VMath.copy(pm), r2 = VMath.copy(pm); + VMath.plusTimesEquals(r1, p1, -l1 + r11); + VMath.plusTimesEquals(r1, p2, -l2 + r12); + VMath.plusTimesEquals(r2, p1, -l1 + r21); + VMath.plusTimesEquals(r2, p2, -l2 + r22); + // Project to render space: + range1 = proj.fastProjectDataToRenderSpace(r1); + range2 = proj.fastProjectDataToRenderSpace(r2); + } + + // Continue lines to viewport. + { + CanvasSize viewport = proj.estimateViewport(); + VMath.minusEquals(range1, pointOfOrigin); + VMath.minusEquals(range2, pointOfOrigin); + VMath.timesEquals(range1, viewport.continueToMargin(pointOfOrigin, range1)); + VMath.timesEquals(range2, viewport.continueToMargin(pointOfOrigin, range2)); + VMath.plusEquals(range1, pointOfOrigin); + VMath.plusEquals(range2, pointOfOrigin); + // Go backwards into the other direction - the origin might not be in the + // viewport! + double[] start1 = VMath.minus(pointOfOrigin, range1); + double[] start2 = VMath.minus(pointOfOrigin, range2); + VMath.timesEquals(start1, viewport.continueToMargin(range1, start1)); + VMath.timesEquals(start2, viewport.continueToMargin(range2, start2)); + VMath.plusEquals(start1, range1); + VMath.plusEquals(start2, range2); + + // TODO: add filled variant? + SVGPath path = new SVGPath(); + path.moveTo(start1); + path.lineTo(range1); + path.moveTo(start2); + path.lineTo(range2); + return path.makeElement(svgp); + } + } + + /** + * Instance, visualizing a particular set of kNNs + * + * @author Robert Rödler + * @author Erich Schubert + * + * @apiviz.has DBIDSelection oneway - - visualizes + */ + public class Instance extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * Generic tags to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String KNNMARKER = "kNNMarker"; + + public static final String KNNDIST = "kNNDist"; + + public static final String DISTANCEFUNCTION = "distancefunction"; + + /** + * The selection result we work on + */ + private AbstractMaterializeKNNPreprocessor<? extends NumberVector> result; + + /** + * Constructor + * + * @param task VisualizationTask + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.result = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + addCSSClasses(svgp); + final double p = getLPNormP(result); + final boolean angular = isAngularDistance(result); + + final double size = style.getSize(StyleLibrary.SELECTION); + DBIDSelection selContext = context.getSelection(); + if(selContext != null) { + DBIDs selection = selContext.getSelectedIds(); + + for(DBIDIter i = selection.iter(); i.valid(); i.advance()) { + final KNNList knn = result.get(i); + for(DoubleDBIDListIter iter = knn.iter(); iter.valid(); iter.advance()) { + try { + double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter)); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + Element dot = svgp.svgCircle(v[0], v[1], size); + SVGUtil.addCSSClass(dot, KNNMARKER); + layer.appendChild(dot); + + Element lbl = svgp.svgText(v[0] + size, v[1] + size, Double.toString(iter.doubleValue())); + SVGUtil.addCSSClass(lbl, KNNDIST); + layer.appendChild(lbl); + } + catch(ObjectNotFoundException e) { + // ignore + } + } + // Last element + DoubleDBIDPair last = knn.get(knn.size() - 1); + // Draw hypersphere if possible + { + final Element dist; + if(p == 1.0) { + dist = SVGHyperSphere.drawManhattan(svgp, proj, rel.get(i), last.doubleValue()); + } + else if(p == 2.0) { + dist = SVGHyperSphere.drawEuclidean(svgp, proj, rel.get(i), last.doubleValue()); + } + else if(!Double.isNaN(p)) { + dist = SVGHyperSphere.drawLp(svgp, proj, rel.get(i), last.doubleValue(), p); + } + else if(angular) { + final NumberVector refvec = rel.get(i); + // Recompute the angle - it could be cosine or arccosine distance + double maxangle = Math.acos(VectorUtil.cosAngle(refvec, rel.get(last))); + dist = drawCosine(svgp, proj, refvec, maxangle); + } + else { + dist = null; + } + if(dist != null) { + SVGUtil.addCSSClass(dist, DISTANCEFUNCTION); + layer.appendChild(dist); + } + } + } + } + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + // Class for the distance markers + if(!svgp.getCSSClassManager().contains(KNNMARKER)) { + CSSClass cls = new CSSClass(this, KNNMARKER); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_DARKGREEN_VALUE); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + svgp.addCSSClassOrLogError(cls); + } + // Class for the distance function + if(!svgp.getCSSClassManager().contains(DISTANCEFUNCTION)) { + CSSClass cls = new CSSClass(this, DISTANCEFUNCTION); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_RED_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + svgp.addCSSClassOrLogError(cls); + } + // Class for the distance label + if(!svgp.getCSSClassManager().contains(KNNDIST)) { + CSSClass cls = new CSSClass(this, KNNDIST); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLACK_VALUE); + cls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.PLOT)); + svgp.addCSSClassOrLogError(cls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/MoveObjectsToolVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/MoveObjectsToolVisualization.java new file mode 100644 index 00000000..c594d500 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/MoveObjectsToolVisualization.java @@ -0,0 +1,239 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.Database; +import de.lmu.ifi.dbs.elki.database.UpdatableDatabase; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea.DragListener; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Tool to move the currently selected objects. + * + * @author Heidi Kolb + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class MoveObjectsToolVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Move Objects"; + + /** + * Constructor + */ + public MoveObjectsToolVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Database db = ResultUtil.findDatabase(context.getHierarchy()); + if(!(db instanceof UpdatableDatabase)) { + return; + } + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), rel, MoveObjectsToolVisualization.this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.tool = true; + task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT); + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_SELECTION); + task.initDefaultVisibility(false); + // baseResult.getHierarchy().add(p.getRelation(), task); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Heidi Kolb + * @author Erich Schubert + * + * @apiviz.has de.lmu.ifi.dbs.elki.data.NumberVector oneway - - edits + */ + public class Instance extends AbstractScatterplotVisualization implements DragListener { + /** + * CSS tag for our event rectangle + */ + protected static final String CSS_ARROW = "moveArrow"; + + /** + * Element for the rectangle to add listeners + */ + private Element etag; + + /** + * Element to contain the drag arrow + */ + private Element rtag; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + addCSSClasses(svgp); + + rtag = svgp.svgElement(SVGConstants.SVG_G_TAG); + SVGUtil.addCSSClass(rtag, CSS_ARROW); + layer.appendChild(rtag); + + DragableArea drag = new DragableArea(svgp, -0.6 * StyleLibrary.SCALE, -0.7 * StyleLibrary.SCALE, 1.3 * StyleLibrary.SCALE, 1.4 * StyleLibrary.SCALE, this); + etag = drag.getElement(); + layer.appendChild(etag); + } + + /** + * Updates the objects with the given DBIDs It will be moved depending on + * the given Vector + * + * @param dbids - DBIDs of the objects to move + * @param movingVector - Vector for moving object + */ + // TODO: move to DatabaseUtil? + private void updateDB(DBIDs dbids, Vector movingVector) { + throw new AbortException("FIXME: INCOMPLETE TRANSITION"); + /* + * NumberVector nv = null; database.accumulateDataStoreEvents(); + * Representation<DatabaseObjectMetadata> mrep = + * database.getMetadataQuery(); for(DBID dbid : dbids) { NV obj = + * database.get(dbid); // Copy metadata to keep DatabaseObjectMetadata + * meta = mrep.get(dbid); + * + * Vector v = proj.projectDataToRenderSpace(obj); v.set(0, v.get(0) + + * movingVector.get(0)); v.set(1, v.get(1) + movingVector.get(1)); NV nv = + * proj.projectRenderToDataSpace(v, obj); nv.setID(obj.getID()); + * + * try { database.delete(dbid); database.insert(new Pair<NV, + * DatabaseObjectMetadata>(nv, meta)); } catch(UnableToComplyException e) + * { de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e); } } + * database.flushDataStoreEvents(); + */ + } + + /** + * Delete the children of the element + * + * @param container SVG-Element + */ + private void deleteChildren(Element container) { + while(container.hasChildNodes()) { + container.removeChild(container.getLastChild()); + } + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVGPlot + */ + private void addCSSClasses(SVGPlot svgp) { + // Class for the rectangle to add eventListeners + if(!svgp.getCSSClassManager().contains(CSS_ARROW)) { + final CSSClass acls = new CSSClass(this, CSS_ARROW); + final StyleLibrary style = context.getStyleLibrary(); + acls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE)); + acls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.SELECTION_ACTIVE)); + acls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + svgp.addCSSClassOrLogError(acls); + } + } + + @Override + public boolean startDrag(SVGPoint startPoint, Event evt) { + return true; + } + + @Override + public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + deleteChildren(rtag); + rtag.appendChild(svgp.svgLine(startPoint.getX(), startPoint.getY(), dragPoint.getX(), dragPoint.getY())); + return true; + } + + @Override + public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + Vector movingVector = new Vector(2); + movingVector.set(0, dragPoint.getX() - startPoint.getX()); + movingVector.set(1, dragPoint.getY() - startPoint.getY()); + if(context.getSelection() != null) { + updateDB(context.getSelection().getSelectedIds(), movingVector); + } + deleteChildren(rtag); + return true; + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionConvexHullVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionConvexHullVisualization.java new file mode 100644 index 00000000..add2ab8d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionConvexHullVisualization.java @@ -0,0 +1,181 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.spatial.Polygon; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.geometry.GrahamScanConvexHull2D; +import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualizer for generating an SVG-Element containing the convex hull of the + * selected points + * + * @author Robert Rödler + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class SelectionConvexHullVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Selection Hull"; + + /** + * Constructor + */ + public SelectionConvexHullVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionConvexHullVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 2; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance + * + * @author Robert Rödler + * + * @apiviz.has DBIDSelection oneway - - visualizes + * @apiviz.uses GrahamScanConvexHull2D + */ + public class Instance extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String SELECTEDHULL = "selectionConvexHull"; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + addCSSClasses(svgp); + DBIDSelection selContext = context.getSelection(); + if(selContext != null) { + DBIDs selection = selContext.getSelectedIds(); + GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D(); + for(DBIDIter iter = selection.iter(); iter.valid(); iter.advance()) { + try { + final double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter)); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + hull.add(new Vector(v)); + } + catch(ObjectNotFoundException e) { + // ignore + } + } + Polygon chres = hull.getHull(); + if(chres != null && chres.size() >= 3) { + SVGPath path = new SVGPath(chres); + + Element selHull = path.makeElement(svgp); + SVGUtil.addCSSClass(selHull, SELECTEDHULL); + // TODO: use relative selection size for opacity? + layer.appendChild(selHull); + } + } + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + // Class for the dot markers + if(!svgp.getCSSClassManager().contains(SELECTEDHULL)) { + final StyleLibrary style = context.getStyleLibrary(); + CSSClass cls = new CSSClass(this, SELECTEDHULL); + // cls = new CSSClass(this, CONVEXHULL); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, ".25"); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + svgp.addCSSClassOrLogError(cls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionCubeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionCubeVisualization.java new file mode 100644 index 00000000..7ad7b32b --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionCubeVisualization.java @@ -0,0 +1,253 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.HyperBoundingBox; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.result.RangeSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +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.parameterization.Parameterization; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperCube; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualizer for generating an SVG-Element containing a cube as marker + * representing the selected range for each dimension + * + * @author Heidi Kolb + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +// TODO: Does not use the relation. Always enable, but hide in the menu? +public class SelectionCubeVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Selection Range"; + + /** + * Settings + */ + protected Parameterizer settings; + + /** + * Constructor. + * + * @param settings Settings + */ + public SelectionCubeVisualization(Parameterizer settings) { + super(); + this.settings = settings; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionCubeVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 2; + task.addUpdateFlags(VisualizationTask.ON_SELECTION); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Heidi Kolb + * + * @apiviz.has RangeSelection oneway - - visualizes + * @apiviz.uses SVGHyperCube + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String MARKER = "selectionCubeMarker"; + + /** + * CSS class for the filled cube + */ + public static final String CSS_CUBE = "selectionCube"; + + /** + * CSS class for the cube frame + */ + public static final String CSS_CUBEFRAME = "selectionCubeFrame"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + private void addCSSClasses(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + // Class for the cube + if(!svgp.getCSSClassManager().contains(CSS_CUBE)) { + CSSClass cls = new CSSClass(this, CSS_CUBE); + cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + if(settings.nofill) { + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); + } + else { + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + } + svgp.addCSSClassOrLogError(cls); + } + // Class for the cube frame + if(!svgp.getCSSClassManager().contains(CSS_CUBEFRAME)) { + CSSClass cls = new CSSClass(this, CSS_CUBEFRAME); + cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.SELECTION)); + + svgp.addCSSClassOrLogError(cls); + } + } + + /** + * Generates a cube and a frame depending on the selection stored in the + * context + * + * @param svgp The plot + * @param proj The projection + */ + private void setSVGRect(SVGPlot svgp, Projection2D proj) { + DBIDSelection selContext = context.getSelection(); + if(selContext instanceof RangeSelection) { + HyperBoundingBox ranges = ((RangeSelection) selContext).getRanges(); + if(settings.nofill) { + Element r = SVGHyperCube.drawFrame(svgp, proj, ranges); + SVGUtil.setCSSClass(r, CSS_CUBEFRAME); + layer.appendChild(r); + } + else { + Element r = SVGHyperCube.drawFilled(svgp, CSS_CUBE, proj, ranges); + layer.appendChild(r); + } + + } + } + + @Override + public void fullRedraw() { + setupCanvas(); + addCSSClasses(svgp); + DBIDSelection selContext = context.getSelection(); + if(selContext instanceof RangeSelection) { + setSVGRect(svgp, proj); + } + } + } + + /** + * Parameterization class. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Parameterizer extends AbstractParameterizer { + /** + * Flag for half-transparent filling of selection cubes. + * + * <p> + * Key: {@code -selectionrange.nofill} + * </p> + */ + public static final OptionID NOFILL_ID = new OptionID("selectionrange.nofill", "Use wireframe style for selection ranges."); + + /** + * Fill parameter. + */ + protected boolean nofill; + + @Override + protected void makeOptions(Parameterization config) { + super.makeOptions(config); + Flag nofillF = new Flag(NOFILL_ID); + if(config.grab(nofillF)) { + nofill = nofillF.isTrue(); + } + } + + @Override + protected SelectionCubeVisualization makeInstance() { + return new SelectionCubeVisualization(this); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionDotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionDotVisualization.java new file mode 100644 index 00000000..c9aed72f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionDotVisualization.java @@ -0,0 +1,153 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualizer for generating an SVG-Element containing dots as markers + * representing the selected Database's objects. + * + * @author Heidi Kolb + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class SelectionDotVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Selection Markers"; + + /** + * Constructor + */ + public SelectionDotVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionDotVisualization.this); + task.level = VisualizationTask.LEVEL_DATA - 1; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance + * + * @author Heidi Kolb + * + * @apiviz.has DBIDSelection oneway - - visualizes + */ + public class Instance extends AbstractScatterplotVisualization implements DataStoreListener { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + public static final String MARKER = "selectionDotMarker"; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + DBIDSelection selContext = context.getSelection(); + if(selContext == null) { + return; + } + final StyleLibrary style = context.getStyleLibrary(); + // Class for the dot markers + if(!svgp.getCSSClassManager().contains(MARKER)) { + CSSClass cls = new CSSClass(this, MARKER); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION)); + cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION)); + svgp.addCSSClassOrLogError(cls); + } + final double size = style.getSize(StyleLibrary.SELECTION); + for(DBIDIter iter = selContext.getSelectedIds().iter(); iter.valid(); iter.advance()) { + try { + double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter)); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + Element dot = svgp.svgCircle(v[0], v[1], size); + SVGUtil.addCSSClass(dot, MARKER); + layer.appendChild(dot); + } + catch(ObjectNotFoundException e) { + // ignore + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolCubeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolCubeVisualization.java new file mode 100644 index 00000000..0cc56e08 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolCubeVisualization.java @@ -0,0 +1,298 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.data.ModifiableHyperBoundingBox; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.result.RangeSelection; +import de.lmu.ifi.dbs.elki.utilities.BitsUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Tool-Visualization for the tool to select ranges. + * + * TODO: support non-point spatial data + * + * @author Heidi Kolb + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class SelectionToolCubeVisualization extends AbstractVisFactory { + /** + * The logger for this class. + */ + private static final Logging LOG = Logging.getLogger(SelectionToolCubeVisualization.class); + + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Range Selection"; + + /** + * Constructor. + */ + public SelectionToolCubeVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolCubeVisualization.this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.tool = true; + task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT); + task.addUpdateFlags(VisualizationTask.ON_SELECTION); + task.initDefaultVisibility(false); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance. + * + * @author Heidi Kolb + * + * @apiviz.has RangeSelection oneway - - updates + */ + public class Instance extends AbstractScatterplotVisualization implements DragableArea.DragListener { + /** + * Generic tag to indicate the type of element. Used in IDs, CSS-Classes + * etc. + */ + private static final String CSS_RANGEMARKER = "selectionRangeMarker"; + + /** + * Dimension. + */ + private int dim; + + /** + * Element for selection rectangle. + */ + private Element rtag; + + /** + * Element for the rectangle to add listeners. + */ + private Element etag; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + this.dim = RelationUtil.dimensionality(rel); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + addCSSClasses(svgp); + + // rtag: tag for the selected rect + rtag = svgp.svgElement(SVGConstants.SVG_G_TAG); + SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER); + layer.appendChild(rtag); + + // etag: sensitive area + DragableArea drag = new DragableArea(svgp, -0.6 * StyleLibrary.SCALE, -0.7 * StyleLibrary.SCALE, 1.3 * StyleLibrary.SCALE, 1.4 * StyleLibrary.SCALE, this); + etag = drag.getElement(); + layer.appendChild(etag); + } + + /** + * Delete the children of the element. + * + * @param container SVG-Element + */ + private void deleteChildren(Element container) { + while(container.hasChildNodes()) { + container.removeChild(container.getLastChild()); + } + } + + /** + * Set the selected ranges and the mask for the actual dimensions in the + * context. + * + * @param x1 x-value of the first dimension + * @param x2 x-value of the second dimension + * @param y1 y-value of the first dimension + * @param y2 y-value of the second dimension + * @param ranges Ranges to update + */ + private void updateSelectionRectKoordinates(double x1, double x2, double y1, double y2, ModifiableHyperBoundingBox ranges) { + double[] nv1 = proj.fastProjectRenderToDataSpace(x1, y1); + double[] nv2 = proj.fastProjectRenderToDataSpace(x2, y2); + + long[] actDim = proj.getVisibleDimensions2D(); + for(int d = BitsUtil.nextSetBit(actDim, 0); d >= 0; d = BitsUtil.nextSetBit(actDim, d + 1)) { + ranges.setMin(d, Math.min(nv1[d], nv2[d])); + ranges.setMax(d, Math.max(nv1[d], nv2[d])); + } + } + + @Override + public boolean startDrag(SVGPoint startPoint, Event evt) { + return true; + } + + @Override + public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + deleteChildren(rtag); + double x = Math.min(startPoint.getX(), dragPoint.getX()); + double y = Math.min(startPoint.getY(), dragPoint.getY()); + double width = Math.abs(startPoint.getX() - dragPoint.getX()); + double height = Math.abs(startPoint.getY() - dragPoint.getY()); + rtag.appendChild(svgp.svgRect(x, y, width, height)); + return true; + } + + @Override + public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + deleteChildren(rtag); + if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) { + updateSelection(proj, startPoint, dragPoint); + } + return true; + } + + /** + * Update the selection in the context. + * + * @param proj The projection + * @param p1 First Point of the selected rectangle + * @param p2 Second Point of the selected rectangle + */ + private void updateSelection(Projection proj, SVGPoint p1, SVGPoint p2) { + if(p1 == null || p2 == null) { + LOG.warning("no rect selected: p1: " + p1 + " p2: " + p2); + return; + } + + DBIDSelection selContext = context.getSelection(); + ModifiableDBIDs selection; + if(selContext != null) { + selection = DBIDUtil.newHashSet(selContext.getSelectedIds()); + } + else { + selection = DBIDUtil.newHashSet(); + } + ModifiableHyperBoundingBox ranges; + + double x1 = Math.min(p1.getX(), p2.getX()); + double x2 = Math.max(p1.getX(), p2.getX()); + double y1 = Math.max(p1.getY(), p2.getY()); + double y2 = Math.min(p1.getY(), p2.getY()); + + if(selContext instanceof RangeSelection) { + ranges = ((RangeSelection) selContext).getRanges(); + } + else { + ranges = new ModifiableHyperBoundingBox(dim, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + } + updateSelectionRectKoordinates(x1, x2, y1, y2, ranges); + + selection.clear(); + candidates: for(DBIDIter iditer = rel.iterDBIDs(); iditer.valid(); iditer.advance()) { + NumberVector dbTupel = rel.get(iditer); + for(int i = 0; i < dim; i++) { + final double min = ranges.getMin(i), max = ranges.getMax(i); + if(max < Double.POSITIVE_INFINITY || min > Double.NEGATIVE_INFINITY) { + final double v = dbTupel.doubleValue(i); + if(v < min || v > max) { + continue candidates; + } + } + } + selection.add(iditer); + } + context.setSelection(new RangeSelection(selection, ranges)); + } + + /** + * Adds the required CSS-Classes. + * + * @param svgp SVG-Plot + */ + protected void addCSSClasses(SVGPlot svgp) { + // Class for the range marking + if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) { + final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER); + final StyleLibrary style = context.getStyleLibrary(); + rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE)); + rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE)); + svgp.addCSSClassOrLogError(rcls); + } + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolDotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolDotVisualization.java new file mode 100644 index 00000000..04a092b5 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolDotVisualization.java @@ -0,0 +1,280 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.dom.events.DOMMouseEvent; +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; +import org.w3c.dom.events.Event; +import org.w3c.dom.svg.SVGPoint; + +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil; +import de.lmu.ifi.dbs.elki.database.ids.HashSetModifiableDBIDs; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.result.DBIDSelection; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Tool-Visualization for the tool to select objects + * + * @author Heidi Kolb + * + * @apiviz.stereotype factory + * @apiviz.uses Instance - - «create» + */ +public class SelectionToolDotVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Object Selection"; + + /** + * Input modes + * + * @apiviz.exclude + */ + private enum Mode { + REPLACE, ADD, INVERT + } + + /** + * Constructor. + */ + public SelectionToolDotVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + final Relation<?> rel = p.getRelation(); + if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolDotVisualization.this); + task.level = VisualizationTask.LEVEL_INTERACTIVE; + task.tool = true; + task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT); + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION); + task.initDefaultVisibility(false); + context.addVis(context.getSelectionResult(), task); + context.addVis(p, task); + } + } + + /** + * Instance + * + * @author Heidi Kolb + * + * @apiviz.has DBIDSelection oneway - - updates + */ + public class Instance extends AbstractScatterplotVisualization implements DragableArea.DragListener { + /** + * CSS class of the selection rectangle while selecting. + */ + private static final String CSS_RANGEMARKER = "selectionRangeMarker"; + + /** + * Element for selection rectangle + */ + Element rtag; + + /** + * Element for the rectangle to add listeners + */ + Element etag; + + /** + * Constructor. + * + * @param task Task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + addCSSClasses(svgp); + + // + rtag = svgp.svgElement(SVGConstants.SVG_G_TAG); + SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER); + layer.appendChild(rtag); + + // etag: sensitive area + DragableArea drag = new DragableArea(svgp, -0.6 * StyleLibrary.SCALE, -0.7 * StyleLibrary.SCALE, 1.3 * StyleLibrary.SCALE, 1.4 * StyleLibrary.SCALE, this); + etag = drag.getElement(); + layer.appendChild(etag); + } + + /** + * Delete the children of the element + * + * @param container SVG-Element + */ + private void deleteChildren(Element container) { + while(container.hasChildNodes()) { + container.removeChild(container.getLastChild()); + } + } + + @Override + public boolean startDrag(SVGPoint startPoint, Event evt) { + return true; + } + + @Override + public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + deleteChildren(rtag); + double x = Math.min(startPoint.getX(), dragPoint.getX()); + double y = Math.min(startPoint.getY(), dragPoint.getY()); + double width = Math.abs(startPoint.getX() - dragPoint.getX()); + double height = Math.abs(startPoint.getY() - dragPoint.getY()); + rtag.appendChild(svgp.svgRect(x, y, width, height)); + return true; + } + + @Override + public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) { + Mode mode = getInputMode(evt); + deleteChildren(rtag); + if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) { + updateSelection(mode, proj, startPoint, dragPoint); + } + return true; + } + + /** + * Get the current input mode, on each mouse event. + * + * @param evt Mouse event. + * @return current input mode + */ + private Mode getInputMode(Event evt) { + if(evt instanceof DOMMouseEvent) { + DOMMouseEvent domme = (DOMMouseEvent) evt; + // TODO: visual indication of mode possible? + if(domme.getShiftKey()) { + return Mode.ADD; + } + else if(domme.getCtrlKey()) { + return Mode.INVERT; + } + else { + return Mode.REPLACE; + } + } + // Default mode is replace. + return Mode.REPLACE; + } + + /** + * Updates the selection in the context.<br> + * + * @param mode Input mode + * @param proj + * @param p1 first point of the selected rectangle + * @param p2 second point of the selected rectangle + */ + private void updateSelection(Mode mode, Projection2D proj, SVGPoint p1, SVGPoint p2) { + DBIDSelection selContext = context.getSelection(); + // Note: we rely on SET semantics below! + HashSetModifiableDBIDs selection; + if(selContext == null || mode == Mode.REPLACE) { + selection = DBIDUtil.newHashSet(); + } + else { + selection = DBIDUtil.newHashSet(selContext.getSelectedIds()); + } + for(DBIDIter iditer = rel.iterDBIDs(); iditer.valid(); iditer.advance()) { + double[] vec = proj.fastProjectDataToRenderSpace(rel.get(iditer)); + if(vec[0] >= Math.min(p1.getX(), p2.getX()) && vec[0] <= Math.max(p1.getX(), p2.getX()) && vec[1] >= Math.min(p1.getY(), p2.getY()) && vec[1] <= Math.max(p1.getY(), p2.getY())) { + if(mode == Mode.INVERT) { + if(!selection.contains(iditer)) { + selection.add(iditer); + } + else { + selection.remove(iditer); + } + } + else { + // In REPLACE and ADD, add objects. + // The difference was done before by not re-using the selection. + // Since we are using a set, we can just add in any case. + selection.add(iditer); + } + } + } + context.setSelection(new DBIDSelection(selection)); + } + + /** + * Adds the required CSS-Classes + * + * @param svgp SVG-Plot + */ + protected void addCSSClasses(SVGPlot svgp) { + // Class for the range marking + if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) { + final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER); + final StyleLibrary style = context.getStyleLibrary(); + rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE)); + rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE)); + svgp.addCSSClassOrLogError(rcls); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/package-info.java new file mode 100755 index 00000000..5a8bf99e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/package-info.java @@ -0,0 +1,26 @@ +/** + * <p>Visualizers for object selection based on 2D projections.</p> + */ +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainBoundingBoxVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainBoundingBoxVisualization.java new file mode 100644 index 00000000..4e4cca3a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainBoundingBoxVisualization.java @@ -0,0 +1,191 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperCube; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize uncertain objects by their bounding box. + * + * Note: this is currently a hack. Our projection only applies to vector field + * relations currently, and this visualizer activates if such a relation (e.g. a + * sample, or the center of mass) has a parent relation of type UncertainObject. + * But it serves the purpose. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class UncertainBoundingBoxVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Uncertain Bounding Boxes"; + + /** + * Constructor. + */ + public UncertainBoundingBoxVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + Relation<?> r = p.getRelation(); + if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r.getDataTypeInformation())) { + final VisualizationTask task = new VisualizationTask(NAME, context, p, r, this); + task.level = VisualizationTask.LEVEL_DATA; + // task.initDefaultVisibility(false); + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + continue; + } + } + } + + /** + * Instance. + * + * @author Erich Schubert + * + * @apiviz.uses StylingPolicy + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * CSS class for uncertain bounding boxes. + */ + public static final String CSS_CLASS = "uncertainbb"; + + /** + * The representation we visualize + */ + final protected Relation<? extends UncertainObject> rel; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + this.rel = task.getRelation(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final double opac = .1; // Opacity + final StyleLibrary style = context.getStyleLibrary(); + final double lw = .25 * style.getLineWidth(StyleLibrary.PLOT); + final StylingPolicy spol = context.getStylingPolicy(); + final ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); + + if(spol instanceof ClassStylingPolicy) { + ClassStylingPolicy cspol = (ClassStylingPolicy) spol; + for(int cnum = cspol.getMinStyle(); cnum < cspol.getMaxStyle(); cnum++) { + String css = CSS_CLASS + "_" + cnum; + final String color = colors.getColor(cnum); + CSSClass cls = new CSSClass(this, css); + cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, lw); + cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE); + cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color); + cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, opac); + + svgp.addCSSClassOrLogError(cls); + + for(DBIDIter iter = cspol.iterateClass(cnum); iter.valid(); iter.advance()) { + if(!sample.getSample().contains(iter)) { + continue; // TODO: can we test more efficiently than this? + } + try { + final UncertainObject mbr = rel.get(iter); + Element r = SVGHyperCube.drawFrame(svgp, proj, mbr); + SVGUtil.addCSSClass(r, css); + layer.appendChild(r); + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + } + else { + final String STROKE = SVGConstants.CSS_STROKE_PROPERTY + ":"; + // Color-based styling. + for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) { + try { + final UncertainObject mbr = rel.get(iter); + Element r = SVGHyperCube.drawFrame(svgp, proj, mbr); + SVGUtil.addCSSClass(r, CSS_CLASS); + int col = spol.getColorForDBID(iter); + SVGUtil.setAtt(r, SVGConstants.SVG_STYLE_ATTRIBUTE, STROKE + SVGUtil.colorToString(col)); + layer.appendChild(r); + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainInstancesVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainInstancesVisualization.java new file mode 100644 index 00000000..c66d88e3 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainInstancesVisualization.java @@ -0,0 +1,190 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.SimpleTypeInformation; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +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; +import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize a single derived sample from an uncertain database. + * + * Note: this is currently a hack. Our projection only applies to vector field + * relations currently, and this visualizer activates if such a relation (e.g. a + * sample, or the center of mass) has a parent relation of type UncertainObject. + * But it serves the purpose. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class UncertainInstancesVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Uncertain Instance"; + + /** + * Constructor. + */ + public UncertainInstancesVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + // Find a scatter plot visualizing uncertain objects: + ScatterPlotProjector<?> p = it.get(); + Relation<?> r = p.getRelation(); + if(!TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r.getDataTypeInformation())) { + continue; + } + final VisualizationTask task = new VisualizationTask(NAME, context, p, r, this); + task.level = VisualizationTask.LEVEL_DATA; + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + continue; + } + } + + /** + * Instance. + * + * @author Erich Schubert + * + * @apiviz.uses StylingPolicy + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * CSS class for uncertain bounding boxes. + */ + public static final String CSS_CLASS = "uncertain-instances"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + final StylingPolicy spol = context.getStylingPolicy(); + final double size = style.getSize(StyleLibrary.MARKERPLOT); + final MarkerLibrary ml = style.markers(); + + // Only visualize cluster-based policies + if(!(spol instanceof ClusterStylingPolicy)) { + return; + } + ClusterStylingPolicy cspol = (ClusterStylingPolicy) spol; + Clustering<?> c = cspol.getClustering(); + // If this is a sample from the uncertain database, it must have a parent + // relation containing vectors, which is a child to the uncertain + // database. + Hierarchy.Iter<Result> it = context.getHierarchy().iterAncestors(c); + Relation<? extends NumberVector> srel = null; + boolean isChild = false; + for(; it.valid(); it.advance()) { + Result r = it.get(); + if(r == this.rel) { + isChild = true; + } + else if(r instanceof Relation) { + final SimpleTypeInformation<?> type = ((Relation<?>) r).getDataTypeInformation(); + if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(type)) { + @SuppressWarnings("unchecked") + Relation<? extends NumberVector> vr = (Relation<? extends NumberVector>) r; + int dim = RelationUtil.dimensionality(vr); + if(dim == RelationUtil.dimensionality(this.rel)) { + srel = vr; + } + } + } + if(isChild && srel != null) { + break; + } + } + // Nothing found, probably in a different subtree. + if(!isChild || srel == null) { + return; + } + for(int cnum = cspol.getMinStyle(); cnum < cspol.getMaxStyle(); cnum++) { + for(DBIDIter iter = cspol.iterateClass(cnum); iter.valid(); iter.advance()) { + if(!sample.getSample().contains(iter)) { + continue; // TODO: can we test more efficiently than this? + } + try { + final NumberVector vec = srel.get(iter); + double[] v = proj.fastProjectDataToRenderSpace(vec); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + ml.useMarker(svgp, layer, v[0], v[1], cnum, size); + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainSamplesVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainSamplesVisualization.java new file mode 100644 index 00000000..a186d881 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainSamplesVisualization.java @@ -0,0 +1,301 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Random; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.data.type.TypeUtil; +import de.lmu.ifi.dbs.elki.data.uncertain.DiscreteUncertainObject; +import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.math.random.RandomFactory; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector; +import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization; + +/** + * Visualize uncertain objects by multiple samples. + * + * Note: this is currently a hack. Our projection only applies to vector field + * relations currently, and this visualizer activates if such a relation (e.g. a + * sample, or the center of mass) has a parent relation of type UncertainObject. + * But it serves the purpose. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class UncertainSamplesVisualization extends AbstractVisFactory { + /** + * A short name characterizing this Visualizer. + */ + private static final String NAME = "Uncertain Samples"; + + /** + * Number of samples to draw for uncertain objects. + */ + protected int samples = 10; + + /** + * Constructor. + */ + public UncertainSamplesVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height, proj); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class); + for(; it.valid(); it.advance()) { + ScatterPlotProjector<?> p = it.get(); + Relation<?> r = p.getRelation(); + if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r.getDataTypeInformation())) { + final VisualizationTask task = new VisualizationTask(NAME, context, p, r, this); + task.level = VisualizationTask.LEVEL_DATA; + task.initDefaultVisibility(false); + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + continue; + } + Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<Relation<?>>(context.getHierarchy().iterParents(r), Relation.class); + for(; it2.valid(); it2.advance()) { + Relation<?> r2 = it2.get(); + if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r2.getDataTypeInformation())) { + final VisualizationTask task = new VisualizationTask(NAME, context, p, r2, this); + task.level = VisualizationTask.LEVEL_DATA; + task.initDefaultVisibility(false); + task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY); + context.addVis(p, task); + continue; + } + } + } + } + + /** + * Instance. + * + * @author Erich Schubert + * + * @apiviz.uses StylingPolicy + */ + public class Instance extends AbstractScatterplotVisualization { + /** + * CSS class for uncertain bounding boxes. + */ + public static final String CSS_CLASS = "uncertain-sample"; + + /** + * The representation we visualize + */ + final protected Relation<? extends UncertainObject> rel; + + /** + * Random factory. + */ + final protected RandomFactory random = RandomFactory.DEFAULT; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + super(task, plot, width, height, proj); + addListeners(); + this.rel = task.getRelation(); + } + + @Override + public void fullRedraw() { + setupCanvas(); + final StyleLibrary style = context.getStyleLibrary(); + final StylingPolicy spol = context.getStylingPolicy(); + final double size = style.getSize(StyleLibrary.MARKERPLOT); + final double ssize = size / Math.sqrt(samples); + final MarkerLibrary ml = style.markers(); + + Random rand = random.getSingleThreadedRandom(); + + if(spol instanceof ClassStylingPolicy) { + ClassStylingPolicy cspol = (ClassStylingPolicy) spol; + for(int cnum = cspol.getMinStyle(); cnum < cspol.getMaxStyle(); cnum++) { + for(DBIDIter iter = cspol.iterateClass(cnum); iter.valid(); iter.advance()) { + if(!sample.getSample().contains(iter)) { + continue; // TODO: can we test more efficiently than this? + } + try { + final UncertainObject uo = rel.get(iter); + if(uo instanceof DiscreteUncertainObject) { + drawDiscete((DiscreteUncertainObject) uo, ml, cnum, size); + } + else { + drawContinuous(uo, ml, cnum, ssize, rand); + } + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + } + else { + // Color-based styling. + for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) { + try { + final int col = spol.getColorForDBID(iter); + final UncertainObject uo = rel.get(iter); + if(uo instanceof DiscreteUncertainObject) { + drawDiscreteDefault((DiscreteUncertainObject) uo, col, size); + } + else { + drawContinuousDefault(uo, col, size, rand); + } + } + catch(ObjectNotFoundException e) { + // ignore. + } + } + } + } + + /** + * Visualize a discrete uncertain object + * + * @param uo Uncertain object + * @param ml Marker library + * @param cnum Cluster number + * @param size Size + */ + private void drawDiscete(DiscreteUncertainObject uo, MarkerLibrary ml, int cnum, double size) { + final int e = uo.getNumberSamples(); + final double ssize = size * Math.sqrt(e); + for(int i = 0; i < e; i++) { + final NumberVector s = uo.getSample(i); + if(s == null) { + continue; + } + double[] v = proj.fastProjectDataToRenderSpace(s); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + ml.useMarker(svgp, layer, v[0], v[1], cnum, uo.getWeight(i) * ssize); + } + } + + /** + * Visualize random samples + * + * @param uo Uncertain object + * @param ml Marker library + * @param cnum Cluster number + * @param size Marker size + * @param rand Random generator + */ + private void drawContinuous(UncertainObject uo, MarkerLibrary ml, int cnum, double size, Random rand) { + for(int i = 0; i < samples; i++) { + double[] v = proj.fastProjectDataToRenderSpace(uo.drawSample(rand)); + if(v[0] != v[0] || v[1] != v[1]) { + continue; // NaN! + } + ml.useMarker(svgp, layer, v[0], v[1], cnum, size); + } + } + + /** + * String constant. + */ + private final static String FILL = SVGConstants.CSS_FILL_PROPERTY + ":"; + + /** + * Visualize discrete object + * + * @param uo Uncertain object + * @param col Color + * @param size Size + */ + private void drawDiscreteDefault(DiscreteUncertainObject uo, int col, double size) { + final int e = uo.getNumberSamples(); + final double ssize = size * Math.sqrt(e); + for(int i = 0; i < e; i++) { + final NumberVector s = uo.getSample(i); + if(s == null) { + continue; + } + double[] v = proj.fastProjectDataToRenderSpace(s); + Element dot = svgp.svgCircle(v[0], v[1], ssize * uo.getWeight(i)); + SVGUtil.addCSSClass(dot, CSS_CLASS); + SVGUtil.setAtt(dot, SVGConstants.SVG_STYLE_ATTRIBUTE, FILL + SVGUtil.colorToString(col)); + layer.appendChild(dot); + } + } + + /** + * Visualize random samples + * + * @param uo Uncertain object + * @param col Color + * @param size Size + * @param rand Random generator + */ + private void drawContinuousDefault(UncertainObject uo, int col, double size, Random rand) { + for(int i = 0; i < samples; i++) { + double[] v = proj.fastProjectDataToRenderSpace(uo.drawSample(rand)); + Element dot = svgp.svgCircle(v[0], v[1], size); + SVGUtil.addCSSClass(dot, CSS_CLASS); + SVGUtil.setAtt(dot, SVGConstants.SVG_STYLE_ATTRIBUTE, FILL + SVGUtil.colorToString(col)); + layer.appendChild(dot); + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/package-info.java new file mode 100644 index 00000000..f30516f9 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/package-info.java @@ -0,0 +1,27 @@ +/** + * Visualizers for uncertain data. + */ + +/* + 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 <http://www.gnu.org/licenses/>. + */ +package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailThread.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailThread.java new file mode 100644 index 00000000..e43656cf --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailThread.java @@ -0,0 +1,162 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Thread to render thumbnails in the background. + * + * @author Erich Schubert + * + * @apiviz.uses Listener oneway - - signals + */ +public class ThumbnailThread extends Thread { + /** + * Queue of thumbnails to generate. + */ + private Queue<Task> queue = new ConcurrentLinkedQueue<>(); + + /** + * Flag to signal shutdown. + */ + private boolean shutdown = false; + + /** + * The static thumbnail thread. + */ + private static ThumbnailThread THREAD = null; + + /** + * Queue a thumbnail task in a global thumbnail thread. + * + * @param callback Callback + */ + public synchronized static Task QUEUE(Listener callback) { + final Task task = new Task(callback); + if(THREAD != null) { + // TODO: synchronization? + if(THREAD.isAlive()) { + THREAD.queue(task); + return task; + } + } + THREAD = new ThumbnailThread(); + THREAD.queue(task); + THREAD.start(); + return task; + } + + /** + * Remove a pending task from the queue. + * + * @param task Task to remove. + */ + public static void UNQUEUE(Task task) { + if(THREAD != null) { + synchronized(THREAD) { + THREAD.queue.remove(task); + } + } + } + + /** + * Shutdown the thumbnailer thread. + */ + public static synchronized void SHUTDOWN() { + if(THREAD != null && THREAD.isAlive()) { + THREAD.shutdown(); + } + } + + /** + * Queue a new thumbnail task. + * + * @param task Thumbnail task + */ + private void queue(Task task) { + this.queue.add(task); + } + + /** + * Generate a single Thumbnail. + * + * @param ti Visualization task + */ + private void generateThumbnail(Task ti) { + ti.callback.doThumbnail(); + } + + @Override + public void run() { + while(!queue.isEmpty() && !shutdown) { + generateThumbnail(queue.poll()); + } + } + + /** + * Set the shutdown flag. + */ + private void shutdown() { + this.shutdown = true; + queue.clear(); + } + + /** + * A single thumbnailer task. + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + public static class Task { + /** + * Runnable to call back + */ + Listener callback; + + /** + * Constructor. + * + * @param callback Callback when complete + */ + public Task(Listener callback) { + super(); + this.callback = callback; + } + } + + /** + * Listener interface for completed thumbnails. + * + * @author Erich Schubert + */ + public interface Listener { + /** + * Callback when to (re-)compute the thumbnail. + */ + public void doThumbnail(); + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailVisualization.java new file mode 100644 index 00000000..1c0dd760 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailVisualization.java @@ -0,0 +1,224 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.BufferedImage; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener; +import de.lmu.ifi.dbs.elki.logging.Logging; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.result.SamplingResult; +import de.lmu.ifi.dbs.elki.result.SelectionResult; +import de.lmu.ifi.dbs.elki.visualization.VisualizationItem; +import de.lmu.ifi.dbs.elki.visualization.VisualizationListener; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Thumbnail visualization. + * + * @author Erich Schubert + * + * @apiviz.uses ThumbnailThread + */ +public class ThumbnailVisualization extends AbstractVisualization implements ThumbnailThread.Listener, DataStoreListener, VisualizationListener { + /** + * Visualizer factory + */ + protected final VisFactory visFactory; + + /** + * The thumbnail id. + */ + protected int thumbid = -1; + + /** + * Pending redraw + */ + protected ThumbnailThread.Task pendingThumbnail = null; + + /** + * Thumbnail resolution + */ + protected int tresolution; + + /** + * Our thumbnail (keep a reference to prevent garbage collection!) + */ + private BufferedImage thumb; + + /** + * Plot the thumbnail is in. + */ + private SVGPlot plot; + + /** + * Projection. + */ + private Projection proj; + + /** + * Constructor. + * + * @param visFactory Visualizer Factory to use + * @param task Task to use + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + * @param proj Projection + * @param thumbsize Thumbnail size + */ + public ThumbnailVisualization(VisFactory visFactory, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj, int thumbsize) { + super(task, plot, width, height); + this.visFactory = visFactory; + this.plot = plot; + this.proj = proj; + this.tresolution = thumbsize; + this.layer = plot.svgElement(SVGConstants.SVG_G_TAG); + this.thumbid = -1; + this.thumb = null; + addListeners(); + } + + @Override + public void destroy() { + if(pendingThumbnail != null) { + ThumbnailThread.UNQUEUE(pendingThumbnail); + } + // TODO: remove image from registry? + super.destroy(); + } + + @Override + public Element getLayer() { + if(thumbid < 0) { + svgp.requestRedraw(this.task, this); + } + return layer; + } + + /** + * Perform a full redraw. + */ + @Override + public void fullRedraw() { + if(!(getWidth() > 0 && getHeight() > 0)) { + LoggingUtil.warning("Thumbnail of zero size requested: " + visFactory); + return; + } + if(thumbid < 0) { + // LoggingUtil.warning("Generating new thumbnail " + this); + layer.appendChild(SVGUtil.svgWaitIcon(plot.getDocument(), 0, 0, getWidth(), getHeight())); + if(pendingThumbnail == null) { + pendingThumbnail = ThumbnailThread.QUEUE(this); + } + return; + } + // LoggingUtil.warning("Injecting Thumbnail " + this); + Element i = plot.svgElement(SVGConstants.SVG_IMAGE_TAG); + SVGUtil.setAtt(i, SVGConstants.SVG_X_ATTRIBUTE, 0); + SVGUtil.setAtt(i, SVGConstants.SVG_Y_ATTRIBUTE, 0); + SVGUtil.setAtt(i, SVGConstants.SVG_WIDTH_ATTRIBUTE, getWidth()); + SVGUtil.setAtt(i, SVGConstants.SVG_HEIGHT_ATTRIBUTE, getHeight()); + i.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, ThumbnailRegistryEntry.INTERNAL_PROTOCOL + ":" + thumbid); + layer.appendChild(i); + } + + @Override + public synchronized void doThumbnail() { + pendingThumbnail = null; + try { + VisualizationPlot plot = new VisualizationPlot(); + plot.getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 " + getWidth() + " " + getHeight()); + + // Work on a clone + Visualization vis = visFactory.makeVisualization(task, plot, getWidth(), getHeight(), proj); + + plot.getRoot().appendChild(vis.getLayer()); + plot.updateStyleElement(); + final int tw = (int) (getWidth() * tresolution); + final int th = (int) (getHeight() * tresolution); + thumb = plot.makeAWTImage(tw, th); + thumbid = ThumbnailRegistryEntry.registerImage(thumb); + // The visualization will not be used anymore. + vis.destroy(); + svgp.requestRedraw(this.task, this); + } + catch(Exception e) { + final Logging logger = Logging.getLogger(task.getFactory().getClass()); + if(logger != null && logger.isDebugging()) { + logger.exception("Thumbnail for " + task.getFactory() + " failed.", e); + } + else { + LoggingUtil.warning("Thumbnail for " + task.getFactory() + " failed - enable debugging to see details."); + } + // TODO: hide the failed image? + } + } + + private void refreshThumbnail() { + // Discard an existing thumbnail + thumbid = -1; + thumb = null; + // TODO: also purge from ThumbnailRegistryEntry? + svgp.requestRedraw(this.task, this); + } + + @Override + public void resultChanged(Result current) { + // Default is to redraw when the result we are attached to changed. + if(task.getResult() == current) { + refreshThumbnail(); + return; + } + if(task.updateOnAny(VisualizationTask.ON_SELECTION) && current instanceof SelectionResult) { + refreshThumbnail(); + return; + } + if(task.updateOnAny(VisualizationTask.ON_SAMPLE) && current instanceof SamplingResult) { + refreshThumbnail(); + return; + } + } + + @Override + public void visualizationChanged(VisualizationItem item) { + if(task.updateOnAny(VisualizationTask.ON_STYLEPOLICY) && item instanceof StylingPolicy) { + refreshThumbnail(); + return; + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/package-info.java new file mode 100755 index 00000000..2031e44e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Thumbnail "Visualizers" (that take care of refreshing thumbnails)</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs;
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/EvaluationVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/EvaluationVisualization.java new file mode 100644 index 00000000..b378c71a --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/EvaluationVisualization.java @@ -0,0 +1,216 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.result.EvaluationResult; +import de.lmu.ifi.dbs.elki.result.Result; +import de.lmu.ifi.dbs.elki.utilities.FormatUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +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; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGScoreBar; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Pseudo-Visualizer, that lists the cluster evaluation results found. + * + * TODO: add indication/warning when values are out-of-bounds. + * + * TODO: Find a nicer solution than the current hack to only display the + * evaluation results for the currently active clustering. + * + * @author Erich Schubert + * @author Sascha Goldhofer + * + * @apiviz.stereotype factory + * @apiviz.uses StaticVisualizationInstance oneway - - «create» + * @apiviz.has EvaluationResult oneway - - visualizes + */ +public class EvaluationVisualization extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "Evaluation Bar Chart"; + + /** + * Constant: width of score bars + */ + private static final double BARLENGTH = 5; + + /** + * Constant: height of score bars + */ + private static final double BARHEIGHT = 0.7; + + /** + * Constructor. + */ + public EvaluationVisualization() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<EvaluationResult> it = VisualizationTree.filterResults(context, start, EvaluationResult.class); + candidate: for(; it.valid(); it.advance()) { + EvaluationResult sr = it.get(); + // Avoid duplicates: + Hierarchy.Iter<VisualizationTask> it2 = VisualizationTree.filter(context, sr, VisualizationTask.class); + for(; it2.valid(); it2.advance()) { + if(it2.get().getFactory() instanceof EvaluationVisualization) { + continue candidate; + } + } + // Hack: for clusterings, only show the currently visible clustering. + if(sr.visualizeSingleton()) { + Class<? extends EvaluationResult> c = sr.getClass(); + // Ensure singleton. + Hierarchy.Iter<?> it3 = context.getVisHierarchy().iterChildren(context.getBaseResult()); + for(; it3.valid(); it3.advance()) { + Object o = it3.get(); + if(!(o instanceof VisualizationTask)) { + continue; + } + final VisualizationTask otask = (VisualizationTask) o; + if(otask.getFactory() instanceof EvaluationVisualization && otask.getResult() == c) { + continue candidate; + } + } + final VisualizationTask task = new VisualizationTask(NAME, context, c, null, EvaluationVisualization.this); + task.reqwidth = .5; + task.reqheight = sr.numLines() * .05; + task.level = VisualizationTask.LEVEL_STATIC; + task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY); + context.addVis(context.getBaseResult(), task); + continue candidate; + } + final VisualizationTask task = new VisualizationTask(NAME, context, sr, null, EvaluationVisualization.this); + task.reqwidth = .5; + task.reqheight = sr.numLines() * .05; + task.level = VisualizationTask.LEVEL_STATIC; + context.addVis(sr, task); + } + } + + private double addBarChart(SVGPlot svgp, Element parent, double ypos, String label, double value, double minValue, double maxValue, double baseValue, boolean reversed) { + SVGScoreBar barchart = new SVGScoreBar(); + barchart.setFill(value, baseValue == baseValue ? baseValue : minValue, maxValue); + barchart.setReversed(reversed); + barchart.showValues(FormatUtil.NF4); + barchart.addLabel(label); + parent.appendChild(barchart.build(svgp, 0.0, ypos, BARLENGTH, BARHEIGHT)); + ypos += 1; + return ypos; + } + + private double addHeader(SVGPlot svgp, Element parent, double ypos, String text) { + ypos += .5; + Element object = svgp.svgText(0, ypos + BARHEIGHT * 0.5, text); + object.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6; font-weight: bold"); + parent.appendChild(object); + ypos += 1; + return ypos; + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + // TODO: make a utility class to wrap SVGPlot + parent layer + ypos. + // TODO: use CSSClass and StyleLibrary + + double ypos = -.5; // Skip space before first header + Element parent = plot.svgElement(SVGConstants.SVG_G_TAG); + Object o = task.getResult(); + EvaluationResult sr = null; + if(o instanceof EvaluationResult) { + sr = (EvaluationResult) o; + } + else if(o instanceof Class) { + // Use cluster evaluation of current style instead. + VisualizerContext context = task.getContext(); + StylingPolicy spol = context.getStylingPolicy(); + if(spol instanceof ClusterStylingPolicy) { + ClusterStylingPolicy cpol = (ClusterStylingPolicy) spol; + @SuppressWarnings("unchecked") + final Class<Object> c = (Class<Object>) o; + Iter<?> it = VisualizationTree.filterResults(context, cpol.getClustering(), c); + candidates: for(; it.valid(); it.advance()) { + // This could be attached to a child clustering, in which case we + // may end up displaying the wrong evaluation. + Iter<Result> it2 = context.getHierarchy().iterAncestors((EvaluationResult) it.get()); + for(; it2.valid(); it2.advance()) { + if(it2.get() instanceof Clustering && it2.get() != cpol.getClustering()) { + continue candidates; + } + } + sr = (EvaluationResult) it.get(); + break; + } + } + } + if(sr == null) { + return new StaticVisualizationInstance(task, plot, width, height, parent); // Failed. + } + + for(String header : sr.getHeaderLines()) { + ypos = addHeader(plot, parent, ypos, header); + } + + for(EvaluationResult.MeasurementGroup g : sr) { + ypos = addHeader(plot, parent, ypos, g.getName()); + for(EvaluationResult.Measurement m : g) { + ypos = addBarChart(plot, parent, ypos, m.getName(), m.getVal(), m.getMin(), m.getMax(), m.getExp(), m.lowerIsBetter()); + } + } + + // scale vis + double cols = 10; + final StyleLibrary style = task.getContext().getStyleLibrary(); + final double margin = style.getSize(StyleLibrary.MARGIN); + final String transform = SVGUtil.makeMarginTransform(width, height, cols, ypos, margin / StyleLibrary.SCALE); + SVGUtil.setAtt(parent, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + return new StaticVisualizationInstance(task, plot, width, height, parent); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/HistogramVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/HistogramVisualization.java new file mode 100644 index 00000000..1d3e8771 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/HistogramVisualization.java @@ -0,0 +1,181 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.NumberVector; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.DoubleMinMax; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.result.HistogramResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualizer to draw histograms. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses StaticVisualizationInstance oneway - - «create» + * @apiviz.has HistogramResult oneway - - visualizes + */ +public class HistogramVisualization extends AbstractVisFactory { + /** + * Histogram visualizer name + */ + private static final String NAME = "Histogram"; + + /** + * CSS class name for the series. + */ + private static final String SERIESID = "series"; + + /** + * Constructor. + */ + public HistogramVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + VisualizerContext context = task.getContext(); + HistogramResult<? extends NumberVector> curve = task.getResult(); + + final StyleLibrary style = context.getStyleLibrary(); + final double sizex = StyleLibrary.SCALE; + final double sizey = StyleLibrary.SCALE * height / width; + final double margin = style.getSize(StyleLibrary.MARGIN); + Element layer = SVGUtil.svgElement(plot.getDocument(), SVGConstants.SVG_G_TAG); + final String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + // find maximum, determine step size + int dim = -1; + DoubleMinMax xminmax = new DoubleMinMax(); + DoubleMinMax yminmax = new DoubleMinMax(); + for(NumberVector vec : curve) { + xminmax.put(vec.doubleValue(0)); + if(dim < 0) { + dim = vec.getDimensionality(); + } + else { + // TODO: test and throw always + assert(dim == vec.getDimensionality()); + } + for(int i = 1; i < dim; i++) { + yminmax.put(vec.doubleValue(i)); + } + } + // Minimum should always start at 0 for histograms + yminmax.put(0.0); + // remove one dimension which are the x values. + dim = dim - 1; + + int size = curve.size(); + double range = xminmax.getMax() - xminmax.getMin(); + double binwidth = range / (size - 1); + + LinearScale xscale = new LinearScale(xminmax.getMin() - binwidth * .49999, xminmax.getMax() + binwidth * .49999); + LinearScale yscale = new LinearScale(yminmax.getMin(), yminmax.getMax()); + + SVGPath[] path = new SVGPath[dim]; + for(int i = 0; i < dim; i++) { + path[i] = new SVGPath(sizex * xscale.getScaled(xminmax.getMin() - binwidth * .5), sizey); + } + + // draw curves. + for(NumberVector vec : curve) { + for(int d = 0; d < dim; d++) { + path[d].lineTo(sizex * (xscale.getScaled(vec.doubleValue(0) - binwidth * .5)), sizey * (1 - yscale.getScaled(vec.doubleValue(d + 1)))); + path[d].lineTo(sizex * (xscale.getScaled(vec.doubleValue(0) + binwidth * .5)), sizey * (1 - yscale.getScaled(vec.doubleValue(d + 1)))); + } + } + + // close all histograms + for(int i = 0; i < dim; i++) { + path[i].lineTo(sizex * xscale.getScaled(xminmax.getMax() + binwidth * .5), sizey); + } + + // add axes + try { + SVGSimpleLinearAxis.drawAxis(plot, layer, yscale, 0, sizey, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style); + SVGSimpleLinearAxis.drawAxis(plot, layer, xscale, 0, sizey, sizex, sizey, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style); + } + catch(CSSNamingConflict e) { + LoggingUtil.exception(e); + } + // Setup line styles and insert lines. + ColorLibrary cl = style.getColorSet(StyleLibrary.PLOT); + for(int d = 0; d < dim; d++) { + CSSClass csscls = new CSSClass(this, SERIESID + "_" + d); + csscls.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE); + csscls.setStatement(SVGConstants.SVG_STROKE_ATTRIBUTE, cl.getColor(d)); + csscls.setStatement(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, style.getLineWidth(StyleLibrary.PLOT)); + plot.addCSSClassOrLogError(csscls); + + Element line = path[d].makeElement(plot); + line.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, csscls.getName()); + layer.appendChild(line); + } + + return new StaticVisualizationInstance(task, plot, width, height, layer); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<HistogramResult<?>> it = VisualizationTree.filterResults(context, start, HistogramResult.class); + for(; it.valid(); it.advance()) { + HistogramResult<?> histogram = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, histogram, null, HistogramVisualization.this); + task.reqwidth = 2.0; + task.reqheight = 1.0; + task.level = VisualizationTask.LEVEL_STATIC; + context.addVis(histogram, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // TODO: depending on the histogram complexity? + return false; + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/KeyVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/KeyVisualization.java new file mode 100644 index 00000000..7361bd4e --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/KeyVisualization.java @@ -0,0 +1,334 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; + +import java.util.List; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.data.Cluster; +import de.lmu.ifi.dbs.elki.data.Clustering; +import de.lmu.ifi.dbs.elki.data.model.Model; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter; +import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleDoublePair; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +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; +import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualizer, displaying the key for a clustering. + * + * TODO: re-add automatic sizing depending on the number of clusters. + * + * TODO: also show in scatter plot detail view. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class KeyVisualization extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "Cluster Key"; + + @Override + public void processNewResult(VisualizerContext context, Object start) { + // Ensure there is a clustering result: + Hierarchy.Iter<Clustering<?>> it = VisualizationTree.filterResults(context, start, Clustering.class); + if(!it.valid()) { + return; + } + Hierarchy.Iter<VisualizationTask> i2 = VisualizationTree.filter(context, VisualizationTask.class); + for(; i2.valid(); i2.advance()) { + if(i2.get().getFactory() instanceof KeyVisualization) { + return; // At most one key per plot. + } + } + final VisualizationTask task = new VisualizationTask(NAME, context, context.getStylingPolicy(), null, this); + task.level = VisualizationTask.LEVEL_STATIC; + task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY); + task.reqwidth = 1.; + task.reqheight = 1.; + context.addVis(context.getStylingPolicy(), task); + } + + /** + * Compute the size of the clustering. + * + * @param c Clustering + * @return Array storing the depth and the number of leaf nodes. + */ + protected static <M extends Model> int[] findDepth(Clustering<M> c) { + final Hierarchy<Cluster<M>> hier = c.getClusterHierarchy(); + int[] size = { 0, 0 }; + for(Iter<Cluster<M>> iter = c.iterToplevelClusters(); iter.valid(); iter.advance()) { + findDepth(hier, iter.get(), size); + } + return size; + } + + /** + * Recursive depth computation. + * + * @param hier Hierarchy + * @param cluster Current cluster + * @param size Counting array. + */ + private static <M extends Model> void findDepth(Hierarchy<Cluster<M>> hier, Cluster<M> cluster, int[] size) { + if(hier.numChildren(cluster) > 0) { + for(Iter<Cluster<M>> iter = hier.iterChildren(cluster); iter.valid(); iter.advance()) { + findDepth(hier, iter.get(), size); + } + size[0] += 1; // Depth + } + else { + size[1] += 1; // Leaves + } + } + + /** + * Compute the preferred number of columns. + * + * @param width Target width + * @param height Target height + * @param numc Number of clusters + * @param maxwidth Max width of entries + * @return Preferred number of columns + */ + protected static int getPreferredColumns(double width, double height, int numc, double maxwidth) { + // Maximum width (compared to height) of labels - guess. + // FIXME: do we really need to do this three-step computation? + // Number of rows we'd use in a squared layout: + final double rows = Math.ceil(Math.pow(numc * maxwidth, height / (width + height))); + // Given this number of rows (plus one for header), use this many columns: + return (int) Math.ceil(numc / (rows + 1)); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + return false; + } + + /** + * Instance + * + * @author Erich Schubert + * + * @apiviz.has Clustering oneway - - visualizes + */ + public class Instance extends AbstractVisualization { + /** + * CSS class for key captions. + */ + private static final String KEY_CAPTION = "key-caption"; + + /** + * CSS class for key entries. + */ + private static final String KEY_ENTRY = "key-entry"; + + /** + * CSS class for hierarchy plot lines + */ + private static final String KEY_HIERLINE = "key-hierarchy"; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height) { + super(task, plot, width, height); + addListeners(); + } + + @Override + public void fullRedraw() { + StylingPolicy pol = context.getStylingPolicy(); + if(!(pol instanceof ClusterStylingPolicy)) { + Element label = svgp.svgText(0.1, 0.7, "No clustering selected."); + SVGUtil.setCSSClass(label, KEY_CAPTION); + layer.appendChild(label); + return; + } + @SuppressWarnings("unchecked") + Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) pol).getClustering(); + + StyleLibrary style = context.getStyleLibrary(); + MarkerLibrary ml = style.markers(); + final List<Cluster<Model>> allcs = clustering.getAllClusters(); + final List<Cluster<Model>> topcs = clustering.getToplevelClusters(); + + setupCSS(svgp); + layer = svgp.svgElement(SVGConstants.SVG_G_TAG); + // Add a label for the clustering. + { + Element label = svgp.svgText(0.1, 0.7, clustering.getLongName()); + SVGUtil.setCSSClass(label, KEY_CAPTION); + layer.appendChild(label); + } + + double kwi, khe; + if(allcs.size() == topcs.size()) { + // Maximum width (compared to height) of labels - guess. + // FIXME: compute from labels? + final double maxwidth = 10.; + + // Flat clustering. Use multiple columns. + final int numc = allcs.size(); + final int cols = getPreferredColumns(getWidth(), getHeight(), numc, maxwidth); + final int rows = (int) Math.ceil(numc / (double) cols); + // We use a coordinate system based on rows, so columns are at + // c*maxwidth + + int i = 0; + for(Cluster<Model> c : allcs) { + final int col = i / rows; + final int row = i % rows; + ml.useMarker(svgp, layer, 0.3 + maxwidth * col, row + 1.5, i, 0.3); + Element label = svgp.svgText(0.7 + maxwidth * col, row + 1.7, c.getNameAutomatic()); + SVGUtil.setCSSClass(label, KEY_ENTRY); + layer.appendChild(label); + i++; + } + kwi = cols * maxwidth; + khe = rows; + } + else { + // For consistent keying: + TObjectIntMap<Cluster<Model>> cnum = new TObjectIntHashMap<>(allcs.size()); + int i = 0; + for(Cluster<Model> c : allcs) { + cnum.put(c, i); + i++; + } + // Hierarchical clustering. Draw recursively. + DoubleDoublePair size = new DoubleDoublePair(0., 1.), pos = new DoubleDoublePair(0., 1.); + Hierarchy<Cluster<Model>> hier = clustering.getClusterHierarchy(); + for(Cluster<Model> cluster : topcs) { + drawHierarchy(svgp, ml, size, pos, 0, cluster, cnum, hier); + } + kwi = size.first; + khe = size.second; + } + + final double margin = style.getSize(StyleLibrary.MARGIN); + final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), kwi, khe, margin / StyleLibrary.SCALE); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + } + + private double drawHierarchy(SVGPlot svgp, MarkerLibrary ml, DoubleDoublePair size, DoubleDoublePair pos, int depth, Cluster<Model> cluster, TObjectIntMap<Cluster<Model>> cnum, Hierarchy<Cluster<Model>> hier) { + final double maxwidth = 8.; + DoubleDoublePair subpos = new DoubleDoublePair(pos.first + maxwidth, pos.second); + int numc = hier.numChildren(cluster); + double posy; + if(numc > 0) { + double[] mids = new double[numc]; + Iter<Cluster<Model>> iter = hier.iterChildren(cluster); + for(int i = 0; iter.valid(); iter.advance(), i++) { + mids[i] = drawHierarchy(svgp, ml, size, subpos, depth, iter.get(), cnum, hier); + } + // Center: + posy = (pos.second + subpos.second) * .5; + for(int i = 0; i < numc; i++) { + Element line = svgp.svgLine(pos.first + maxwidth - 1., posy + .5, pos.first + maxwidth, mids[i] + .5); + SVGUtil.setCSSClass(line, KEY_HIERLINE); + layer.appendChild(line); + } + // Use vertical extends of children: + pos.second = subpos.second; + } + else { + posy = pos.second + .5; + pos.second += 1.; + } + ml.useMarker(svgp, layer, 0.3 + pos.first, posy + 0.5, cnum.get(cluster), 0.3); + Element label = svgp.svgText(0.7 + pos.first, posy + 0.7, cluster.getNameAutomatic()); + SVGUtil.setCSSClass(label, KEY_ENTRY); + layer.appendChild(label); + size.first = Math.max(size.first, pos.first + maxwidth); + size.second = Math.max(size.second, pos.second); + return posy; + } + + /** + * Registers the Tooltip-CSS-Class at a SVGPlot. + * + * @param svgp the SVGPlot to register the Tooltip-CSS-Class. + */ + protected void setupCSS(SVGPlot svgp) { + final StyleLibrary style = context.getStyleLibrary(); + final double fontsize = style.getTextSize(StyleLibrary.KEY); + final String fontfamily = style.getFontFamily(StyleLibrary.KEY); + final String color = style.getColor(StyleLibrary.KEY); + + CSSClass keycaption = new CSSClass(svgp, KEY_CAPTION); + keycaption.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + keycaption.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + keycaption.setStatement(SVGConstants.CSS_FILL_PROPERTY, color); + keycaption.setStatement(SVGConstants.CSS_FONT_WEIGHT_PROPERTY, SVGConstants.CSS_BOLD_VALUE); + svgp.addCSSClassOrLogError(keycaption); + + CSSClass keyentry = new CSSClass(svgp, KEY_ENTRY); + keyentry.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize); + keyentry.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily); + keyentry.setStatement(SVGConstants.CSS_FILL_PROPERTY, color); + svgp.addCSSClassOrLogError(keyentry); + + CSSClass hierline = new CSSClass(svgp, KEY_HIERLINE); + hierline.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color); + hierline.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth("key.hierarchy") / StyleLibrary.SCALE); + svgp.addCSSClassOrLogError(hierline); + + svgp.updateStyleElement(); + } + } +} diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/LabelVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/LabelVisualization.java new file mode 100644 index 00000000..c89d9c75 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/LabelVisualization.java @@ -0,0 +1,122 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Trivial "visualizer" that displays a static label. The visualizer is meant to + * be used for dimension labels in the overview. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses StaticVisualizationInstance oneway - - «create» + */ +public class LabelVisualization extends AbstractVisFactory { + /** + * The label to render + */ + private String label = "undefined"; + + /** + * Flag to indicate rotated labels (90 deg to the left) + */ + private boolean rotated = false; + + /** + * Constructor. Solely for API purposes (Parameterizable!) + */ + public LabelVisualization() { + super(); + } + + /** + * The actually used constructor - with a static label. + * + * @param label Label to use + */ + public LabelVisualization(String label) { + this(label, false); + } + + /** + * Constructor. + * + * @param label Label to use + * @param rotated Rotated 90 deg to the left + */ + public LabelVisualization(String label, boolean rotated) { + super(); + this.label = label; + this.rotated = rotated; + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + // No auto discovery supported. + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + VisualizerContext context = task.getContext(); + CSSClass cls = new CSSClass(plot, "unmanaged"); + StyleLibrary style = context.getStyleLibrary(); + double fontsize = style.getTextSize("overview.labels") / StyleLibrary.SCALE; + cls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, SVGUtil.fmt(fontsize)); + cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor("overview.labels")); + cls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily("overview.labels")); + + Element layer; + if(!rotated) { + layer = plot.svgText(width * .5, height * .5 + .35 * fontsize, this.label); + SVGUtil.setAtt(layer, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS()); + SVGUtil.setAtt(layer, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); + } + else { + layer = plot.svgText(height * -.5, width * .5 + .35 * fontsize, this.label); + SVGUtil.setAtt(layer, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS()); + SVGUtil.setAtt(layer, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90)"); + } + return new StaticVisualizationInstance(task, plot, width, height, layer); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + return false; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/PixmapVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/PixmapVisualizer.java new file mode 100644 index 00000000..b6fcdd57 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/PixmapVisualizer.java @@ -0,0 +1,146 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.RenderedImage; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.result.PixmapResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualize an arbitrary pixmap result. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class PixmapVisualizer extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "Pixmap Visualizer"; + + /** + * Constructor. + */ + public PixmapVisualizer() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<PixmapResult> it = VisualizationTree.filterResults(context, start, PixmapResult.class); + for(; it.valid(); it.advance()) { + PixmapResult pr = it.get(); + // Add plots, attach visualizer + final VisualizationTask task = new VisualizationTask(NAME, context, pr, null, PixmapVisualizer.this); + task.reqwidth = pr.getImage().getWidth() / (double) pr.getImage().getHeight(); + task.reqheight = 1.0; + task.level = VisualizationTask.LEVEL_STATIC; + context.addVis(pr, task); + } + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance. + * + * @author Erich Schubert + * + * @apiviz.has PixmapResult oneway - 1 visualizes + */ + public class Instance extends AbstractVisualization { + /** + * The actual pixmap result. + */ + private PixmapResult result; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height) { + super(task, plot, width, height); + this.result = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + final double scale = StyleLibrary.SCALE; + final double sizex = scale; + final double sizey = scale * getHeight() / getWidth(); + final double margin = 0.0; // context.getStyleLibrary().getSize(StyleLibrary.MARGIN); + layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); + final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), sizex, sizey, margin); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + RenderedImage img = result.getImage(); + // is ratio, target ratio + double iratio = img.getHeight() / img.getWidth(); + double tratio = getHeight() / getWidth(); + // We want to place a (iratio, 1.0) object on a (tratio, 1.0) screen. + // Both dimensions must fit: + double zoom = (iratio >= tratio) ? Math.min(tratio / iratio, 1.0) : Math.max(iratio / tratio, 1.0); + + Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG); + SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE); + SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, 0); + SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, 0); + SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, scale * zoom * iratio); + SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, scale * zoom); + itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, result.getAsFile().toURI().toString()); + + layer.appendChild(itag); + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SettingsVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SettingsVisualization.java new file mode 100644 index 00000000..3efa04d1 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SettingsVisualization.java @@ -0,0 +1,154 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.util.Collection; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.result.SettingsResult; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.TrackedParameter; +import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ClassParameter; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Pseudo-Visualizer, that lists the settings of the algorithm- + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses StaticVisualizationInstance oneway - - «create» + * @apiviz.has SettingsResult oneway - - visualizes + */ +// TODO: make this a menu item instead of a "visualization"? +public class SettingsVisualization extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "Settings"; + + /** + * Constructor. + */ + public SettingsVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + SettingsResult sr = task.getResult(); + VisualizerContext context = task.getContext(); + + Collection<TrackedParameter> settings = sr.getSettings(); + + Element layer = plot.svgElement(SVGConstants.SVG_G_TAG); + + // FIXME: use CSSClass and StyleLibrary + + int i = 0; + Object last = null; + for(TrackedParameter setting : settings) { + if(setting.getOwner() != last && setting.getOwner() != null) { + String name; + try { + if(setting.getOwner() instanceof Class) { + name = ((Class<?>) setting.getOwner()).getName(); + } + else { + name = setting.getOwner().getClass().getName(); + } + if(ClassParameter.class.isInstance(setting.getOwner())) { + name = ((ClassParameter<?>) setting.getOwner()).getValue().getName(); + } + } + catch(NullPointerException e) { + name = "[null]"; + } + Element object = plot.svgText(0, i + 0.7, name); + object.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6; font-weight: bold"); + layer.appendChild(object); + i++; + last = setting.getOwner(); + } + // get name and value + String name = setting.getParameter().getOptionID().getName(); + String value = "[unset]"; + try { + if(setting.getParameter().isDefined()) { + value = setting.getParameter().getValueAsString(); + } + } + catch(NullPointerException e) { + value = "[null]"; + } + + Element label = plot.svgText(0, i + 0.7, name); + label.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6"); + layer.appendChild(label); + Element vale = plot.svgText(7.5, i + 0.7, value); + vale.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6"); + layer.appendChild(vale); + // only advance once, since we want these two to be in the same line. + i++; + } + + int cols = Math.max(30, (int) (i * height / width)); + int rows = i; + final double margin = context.getStyleLibrary().getSize(StyleLibrary.MARGIN); + final String transform = SVGUtil.makeMarginTransform(width, height, cols, rows, margin / StyleLibrary.SCALE); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + return new StaticVisualizationInstance(task, plot, width, height, layer); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<SettingsResult> it = VisualizationTree.filterResults(context, start, SettingsResult.class); + for(; it.valid(); it.advance()) { + SettingsResult sr = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, sr, null, SettingsVisualization.this); + task.reqwidth = 1.0; + task.reqheight = 1.0; + task.level = VisualizationTask.LEVEL_STATIC; + task.initDefaultVisibility(false); + context.addVis(sr, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + return false; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SimilarityMatrixVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SimilarityMatrixVisualizer.java new file mode 100644 index 00000000..0b64d6c3 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SimilarityMatrixVisualizer.java @@ -0,0 +1,176 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import java.awt.image.RenderedImage; + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.database.Database; +import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; +import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.evaluation.similaritymatrix.ComputeSimilarityMatrixImage.SimilarityMatrix; +import de.lmu.ifi.dbs.elki.result.ResultUtil; +import de.lmu.ifi.dbs.elki.utilities.DatabaseUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualize a similarity matrix with object labels + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses Instance oneway - - «create» + */ +public class SimilarityMatrixVisualizer extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "Similarity Matrix Visualizer"; + + /** + * Constructor. + */ + public SimilarityMatrixVisualizer() { + super(); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<SimilarityMatrix> it = VisualizationTree.filterResults(context, start, SimilarityMatrix.class); + for(; it.valid(); it.advance()) { + SimilarityMatrix pr = it.get(); + // Add plots, attach visualizer + final VisualizationTask task = new VisualizationTask(NAME, context, pr, null, SimilarityMatrixVisualizer.this); + task.reqwidth = 1.0; + task.reqheight = 1.0; + task.level = VisualizationTask.LEVEL_STATIC; + context.addVis(pr, task); + } + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + return new Instance(task, plot, width, height); + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // Don't use thumbnails + return false; + } + + /** + * Instance + * + * @author Erich Schubert + * + * @apiviz.has SimilarityMatrix oneway - 1 visualizes + */ + public class Instance extends AbstractVisualization { + /** + * The actual pixmap result. + */ + private SimilarityMatrix result; + + /** + * Constructor. + * + * @param task Visualization task + * @param plot Plot to draw to + * @param width Embedding width + * @param height Embedding height + */ + public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height) { + super(task, plot, width, height); + this.result = task.getResult(); + addListeners(); + } + + @Override + public void fullRedraw() { + final StyleLibrary style = context.getStyleLibrary(); + final double sizex = StyleLibrary.SCALE; + final double sizey = StyleLibrary.SCALE * getHeight() / getWidth(); + final double margin = style.getSize(StyleLibrary.MARGIN); + layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); + final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), sizex, sizey, margin); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + RenderedImage img = result.getImage(); + // is ratio, target ratio + double iratio = img.getHeight() / img.getWidth(); + double tratio = getHeight() / getWidth(); + // We want to place a (iratio, 1.0) object on a (tratio, 1.0) screen. + // Both dimensions must fit: + double zoom = (iratio >= tratio) ? Math.min(tratio / iratio, 1.0) : Math.max(iratio / tratio, 1.0); + + Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG); + SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE); + SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, margin * 0.75); + SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, margin * 0.75); + SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, StyleLibrary.SCALE * zoom * iratio); + SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, StyleLibrary.SCALE * zoom); + itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, result.getAsFile().toURI().toString()); + layer.appendChild(itag); + + // Add object labels + final int size = result.getIDs().size(); + final double hlsize = StyleLibrary.SCALE * zoom * iratio / size; + final double vlsize = StyleLibrary.SCALE * zoom / size; + int i = 0; + Database database = ResultUtil.findDatabase(context.getHierarchy()); + final Relation<String> lrep = DatabaseUtil.guessObjectLabelRepresentation(database); + for(DBIDIter id = result.getIDs().iter(); id.valid(); id.advance()) { + String label = lrep.get(id); + if(label != null) { + // Label on horizontal axis + final double hlx = margin * 0.75 + hlsize * (i + .8); + final double hly = margin * 0.7; + Element lbl = svgp.svgText(hlx, hly, label); + SVGUtil.setAtt(lbl, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90," + hlx + "," + hly + ")"); + SVGUtil.setAtt(lbl, SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + hlsize * 0.8); + layer.appendChild(lbl); + // Label on vertical axis + Element lbl2 = svgp.svgText(margin * 0.7, margin * 0.75 + vlsize * (i + .8), label); + SVGUtil.setAtt(lbl2, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_END_VALUE); + SVGUtil.setAtt(lbl2, SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + vlsize * 0.8); + layer.appendChild(lbl2); + } + i++; + } + } + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYCurveVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYCurveVisualization.java new file mode 100644 index 00000000..3c620a8f --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYCurveVisualization.java @@ -0,0 +1,205 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.evaluation.outlier.OutlierPrecisionRecallCurve.PRCurve; +import de.lmu.ifi.dbs.elki.evaluation.outlier.OutlierROCCurve; +import de.lmu.ifi.dbs.elki.evaluation.outlier.OutlierROCCurve.ROCResult; +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.geometry.XYCurve; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.utilities.FormatUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualizer to render a simple 2D curve such as a ROC curve. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses StaticVisualizationInstance oneway - - «create» + * @apiviz.has XYCurve oneway - - visualizes + */ +public class XYCurveVisualization extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "XYCurve"; + + /** + * SVG class name for plot line + */ + private static final String SERIESID = "series"; + + /** + * Axis labels + */ + private static final String CSS_AXIS_LABEL = "xy-axis-label"; + + /** + * Constructor, Parameterizable style - does nothing. + */ + public XYCurveVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + VisualizerContext context = task.getContext(); + XYCurve curve = task.getResult(); + + setupCSS(context, plot); + final StyleLibrary style = context.getStyleLibrary(); + final double sizex = StyleLibrary.SCALE; + final double sizey = StyleLibrary.SCALE * height / width; + final double margin = style.getSize(StyleLibrary.MARGIN); + Element layer = SVGUtil.svgElement(plot.getDocument(), SVGConstants.SVG_G_TAG); + final String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + // determine scaling + LinearScale scalex = new LinearScale(curve.getMinx(), curve.getMaxx()); + LinearScale scaley = new LinearScale(curve.getMiny(), curve.getMaxy()); + // plot the line + SVGPath path = new SVGPath(); + for(XYCurve.Itr iterator = curve.iterator(); iterator.valid(); iterator.advance()) { + final double x = scalex.getScaled(iterator.getX()); + final double y = 1 - scaley.getScaled(iterator.getY()); + path.drawTo(sizex * x, sizey * y); + } + Element line = path.makeElement(plot); + line.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, SERIESID); + + // add axes + try { + SVGSimpleLinearAxis.drawAxis(plot, layer, scaley, 0, sizey, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style); + SVGSimpleLinearAxis.drawAxis(plot, layer, scalex, 0, sizey, sizex, sizey, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style); + } + catch(CSSNamingConflict e) { + LoggingUtil.exception(e); + } + // Add axis labels + { + Element labelx = plot.svgText(sizex * .5, sizey + margin * .9, curve.getLabelx()); + SVGUtil.setCSSClass(labelx, CSS_AXIS_LABEL); + layer.appendChild(labelx); + Element labely = plot.svgText(margin * -.8, sizey * .5, curve.getLabely()); + SVGUtil.setCSSClass(labely, CSS_AXIS_LABEL); + SVGUtil.setAtt(labely, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90," + FormatUtil.NF6.format(margin * -.8) + "," + FormatUtil.NF6.format(sizey * .5) + ")"); + layer.appendChild(labely); + } + + // Add AUC value when found + if(curve instanceof ROCResult) { + double rocauc = ((ROCResult) curve).getAUC(); + String lt = OutlierROCCurve.ROCAUC_LABEL + ": " + FormatUtil.NF.format(rocauc); + if(rocauc <= 0.5) { + Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.10, lt); + SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL); + layer.appendChild(auclbl); + } + else { + Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.95, lt); + SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL); + layer.appendChild(auclbl); + } + } + if(curve instanceof PRCurve) { + double prauc = ((PRCurve) curve).getAUC(); + String lt = PRCurve.PRAUC_LABEL + ": " + FormatUtil.NF.format(prauc); + if(prauc <= 0.5) { + Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.10, lt); + SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL); + layer.appendChild(auclbl); + } + else { + Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.95, lt); + SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL); + layer.appendChild(auclbl); + } + } + + layer.appendChild(line); + return new StaticVisualizationInstance(task, plot, width, height, layer); + } + + /** + * Setup the CSS classes for the plot. + * + * @param svgp Plot + */ + private void setupCSS(VisualizerContext context, SVGPlot svgp) { + StyleLibrary style = context.getStyleLibrary(); + CSSClass csscls = new CSSClass(this, SERIESID); + // csscls.setStatement(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "0.2%"); + csscls.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE); + style.lines().formatCSSClass(csscls, 0, style.getLineWidth(StyleLibrary.XYCURVE)); + svgp.addCSSClassOrLogError(csscls); + // Axis label + CSSClass label = new CSSClass(this, CSS_AXIS_LABEL); + label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.XYCURVE)); + label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.XYCURVE)); + label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.XYCURVE)); + label.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.CSS_MIDDLE_VALUE); + svgp.addCSSClassOrLogError(label); + svgp.updateStyleElement(); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<XYCurve> it = VisualizationTree.filterResults(context, start, XYCurve.class); + for(; it.valid(); it.advance()) { + XYCurve curve = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, curve, null, XYCurveVisualization.this); + task.reqwidth = 1.0; + task.reqheight = 1.0; + task.level = VisualizationTask.LEVEL_STATIC; + context.addVis(curve, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // TODO: depending on the curve complexity? + return false; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYPlotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYPlotVisualization.java new file mode 100644 index 00000000..3b07860d --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYPlotVisualization.java @@ -0,0 +1,178 @@ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; + +/* + 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 <http://www.gnu.org/licenses/>. + */ + +import org.apache.batik.util.SVGConstants; +import org.w3c.dom.Element; + +import de.lmu.ifi.dbs.elki.logging.LoggingUtil; +import de.lmu.ifi.dbs.elki.math.geometry.XYPlot; +import de.lmu.ifi.dbs.elki.math.scales.LinearScale; +import de.lmu.ifi.dbs.elki.utilities.FormatUtil; +import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; +import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; +import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; +import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; +import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; +import de.lmu.ifi.dbs.elki.visualization.projections.Projection; +import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; +import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; +import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; +import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance; +import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; + +/** + * Visualizer to render a simple 2D curve such as a ROC curve. + * + * @author Erich Schubert + * + * @apiviz.stereotype factory + * @apiviz.uses StaticVisualizationInstance oneway - - «create» + * @apiviz.has XYPlot oneway - - visualizes + */ +public class XYPlotVisualization extends AbstractVisFactory { + /** + * Name for this visualizer. + */ + private static final String NAME = "XYPlot"; + + /** + * SVG class name for plot line + */ + private static final String SERIESID = "series_"; + + /** + * Axis labels + */ + private static final String CSS_AXIS_LABEL = "xy-axis-label"; + + /** + * Constructor, Parameterizable style - does nothing. + */ + public XYPlotVisualization() { + super(); + } + + @Override + public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { + VisualizerContext context = task.getContext(); + XYPlot xyplot = task.getResult(); + + setupCSS(context, plot, xyplot); + final StyleLibrary style = context.getStyleLibrary(); + final double sizex = StyleLibrary.SCALE; + final double sizey = StyleLibrary.SCALE * height / width; + final double margin = style.getSize(StyleLibrary.MARGIN); + Element layer = SVGUtil.svgElement(plot.getDocument(), SVGConstants.SVG_G_TAG); + final String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin); + SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); + + // determine scaling + LinearScale scalex = new LinearScale(xyplot.getMinx(), xyplot.getMaxx()); + LinearScale scaley = new LinearScale(xyplot.getMiny(), xyplot.getMaxy()); + + for(XYPlot.Curve curve : xyplot) { + // plot the line + SVGPath path = new SVGPath(); + for(XYPlot.Curve.Itr iterator = curve.iterator(); iterator.valid(); iterator.advance()) { + final double x = scalex.getScaled(iterator.getX()); + final double y = 1 - scaley.getScaled(iterator.getY()); + path.drawTo(sizex * x, sizey * y); + } + Element line = path.makeElement(plot); + line.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, SERIESID + curve.getColor()); + layer.appendChild(line); + } + + // add axes + try { + SVGSimpleLinearAxis.drawAxis(plot, layer, scaley, 0, sizey, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style); + SVGSimpleLinearAxis.drawAxis(plot, layer, scalex, 0, sizey, sizex, sizey, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style); + } + catch(CSSNamingConflict e) { + LoggingUtil.exception(e); + } + // Add axis labels + { + Element labelx = plot.svgText(sizex * .5, sizey + margin * .9, xyplot.getLabelx()); + SVGUtil.setCSSClass(labelx, CSS_AXIS_LABEL); + layer.appendChild(labelx); + Element labely = plot.svgText(margin * -.8, sizey * .5, xyplot.getLabely()); + SVGUtil.setCSSClass(labely, CSS_AXIS_LABEL); + SVGUtil.setAtt(labely, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90," + FormatUtil.NF6.format(margin * -.8) + "," + FormatUtil.NF6.format(sizey * .5) + ")"); + layer.appendChild(labely); + } + + return new StaticVisualizationInstance(task, plot, width, height, layer); + } + + /** + * Setup the CSS classes for the plot. + * + * @param svgp Plot + * @param plot Plot to render + */ + private void setupCSS(VisualizerContext context, SVGPlot svgp, XYPlot plot) { + StyleLibrary style = context.getStyleLibrary(); + for(XYPlot.Curve curve : plot) { + CSSClass csscls = new CSSClass(this, SERIESID + curve.getColor()); + // csscls.setStatement(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "0.2%"); + csscls.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE); + style.lines().formatCSSClass(csscls, curve.getColor(), style.getLineWidth(StyleLibrary.XYCURVE)); + svgp.addCSSClassOrLogError(csscls); + } + // Axis label + CSSClass label = new CSSClass(this, CSS_AXIS_LABEL); + label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.XYCURVE)); + label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.XYCURVE)); + label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.XYCURVE)); + label.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.CSS_MIDDLE_VALUE); + svgp.addCSSClassOrLogError(label); + svgp.updateStyleElement(); + } + + @Override + public void processNewResult(VisualizerContext context, Object start) { + Hierarchy.Iter<XYPlot> it = VisualizationTree.filterResults(context, start, XYPlot.class); + for(; it.valid(); it.advance()) { + XYPlot plot = it.get(); + final VisualizationTask task = new VisualizationTask(NAME, context, plot, null, XYPlotVisualization.this); + task.reqwidth = 1.0; + task.reqheight = 1.0; + task.level = VisualizationTask.LEVEL_STATIC; + context.addVis(plot, task); + } + } + + @Override + public boolean allowThumbnails(VisualizationTask task) { + // TODO: depending on the curve complexity? + return false; + } +}
\ No newline at end of file diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/package-info.java new file mode 100755 index 00000000..23e29d19 --- /dev/null +++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/package-info.java @@ -0,0 +1,27 @@ +/** + * <p>Visualizers that do not use a particular projection.</p> + * + */ +/* +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 <http://www.gnu.org/licenses/>. +*/ +package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.application.AbstractApplication b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.application.AbstractApplication new file mode 100644 index 00000000..ef4a2452 --- /dev/null +++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.application.AbstractApplication @@ -0,0 +1 @@ +de.lmu.ifi.dbs.elki.application.greedyensemble.VisualizePairwiseGainMatrix diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.result.ResultHandler b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.result.ResultHandler new file mode 100644 index 00000000..00032d8f --- /dev/null +++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.result.ResultHandler @@ -0,0 +1,2 @@ +de.lmu.ifi.dbs.elki.visualization.gui.ResultVisualizer visualizer vis ResultVisualizer +de.lmu.ifi.dbs.elki.visualization.ExportVisualizations diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor new file mode 100644 index 00000000..3ba543d8 --- /dev/null +++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor @@ -0,0 +1,60 @@ +de.lmu.ifi.dbs.elki.visualization.projector.HistogramFactory +de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotFactory +de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotFactory +de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjectorFactory +de.lmu.ifi.dbs.elki.visualization.visualizers.actions.ClusterStyleAction +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AxisVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.MarkerVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.PolygonVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterMeanVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterStarVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterHullVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.EMClusterVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.VoronoiVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterOrderVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeMBRVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeSphereVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.BubbleVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.COPVectorVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipScoreVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipStringVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.ReferencePointsVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density.DensityEstimationOverlay +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.DistanceFunctionVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.MoveObjectsToolVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionDotVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionConvexHullVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionCubeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolCubeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolDotVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainBoundingBoxVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainSamplesVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainInstancesVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.histogram.ColoredHistogramVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.LineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.BoundingBoxVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.ParallelAxisVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisVisibilityVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisReorderVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterParallelMeanVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterOutlineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index.RTreeParallelVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionAxisRangeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionLineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolAxisRangeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolLineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments.CircleSegmentsVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.HistogramVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.EvaluationVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYCurveVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYPlotVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.PixmapVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SimilarityMatrixVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.KeyVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SettingsVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSClusterVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotCutVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotSelectionVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSSteepAreaVisualization diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.projector.ProjectorFactory b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.projector.ProjectorFactory new file mode 100644 index 00000000..5fc4f399 --- /dev/null +++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.projector.ProjectorFactory @@ -0,0 +1,4 @@ +de.lmu.ifi.dbs.elki.visualization.projector.HistogramFactory +de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotFactory +de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotFactory +de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjectorFactory
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory new file mode 100644 index 00000000..19c05f02 --- /dev/null +++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory @@ -0,0 +1,56 @@ +de.lmu.ifi.dbs.elki.visualization.visualizers.actions.ClusterStyleAction +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AxisVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.MarkerVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.PolygonVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterMeanVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterStarVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterHullVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.EMClusterVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.VoronoiVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterOrderVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeMBRVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeSphereVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.BubbleVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.COPVectorVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipScoreVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipStringVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.ReferencePointsVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density.DensityEstimationOverlay +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.DistanceFunctionVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.MoveObjectsToolVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionDotVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionConvexHullVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionCubeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolCubeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolDotVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainBoundingBoxVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainSamplesVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainInstancesVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.histogram.ColoredHistogramVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.LineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.BoundingBoxVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.ParallelAxisVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisVisibilityVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisReorderVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterParallelMeanVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterOutlineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index.RTreeParallelVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionAxisRangeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionLineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolAxisRangeVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolLineVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments.CircleSegmentsVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.HistogramVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.EvaluationVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYCurveVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYPlotVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.PixmapVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SimilarityMatrixVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.KeyVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SettingsVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSClusterVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotCutVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotSelectionVisualization +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotVisualizer +de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSSteepAreaVisualization diff --git a/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.ext.awt.image.spi.RegistryEntry b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.ext.awt.image.spi.RegistryEntry new file mode 100644 index 00000000..f1252025 --- /dev/null +++ b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.ext.awt.image.spi.RegistryEntry @@ -0,0 +1 @@ +de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.util.ParsedURLProtocolHandler b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.util.ParsedURLProtocolHandler new file mode 100644 index 00000000..f1252025 --- /dev/null +++ b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.util.ParsedURLProtocolHandler @@ -0,0 +1 @@ +de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/classic.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/classic.properties new file mode 100644 index 00000000..66df4720 --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/classic.properties @@ -0,0 +1,79 @@ +## Libraries +lines-library = SolidLineStyleLibrary +marker-library = PrettyMarkers + +## Default foreground color +color=black +## Default font family +font-family='Times New Roman', serif +## Default background color +background-color=white +## Default line width +line-width=0.01 +## Text font size scale +text-size=0.02 +## Text color +text-color=black +## Default margin (relative) +margin.size=0.1 + +## Named color for the page background +page.background-color=white +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=black +## Named color for a typical axis tick mark +axis.tick.color=grey +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=black +## Axis line width +axis.line-width=0.002 +## Axis tick width +axis.tick.line-width=0.002 +## Axis label font size +axis.label.text-size=0.02 + +## Named color for a label in the key part +key.text-size=0.007 +key.color=black +key.hierarchy.line-width=0.05 + +## A list of color names for data lines +# We stick to primary colors first to have the least issues +# The first two colors are red and blue to help red-green blind people. +# Yellow usually offers bad contrast, therefore comes late. +# Magenta often shows up too similar to red, cyan too similar to blue in print. +colorset=red,blue,green,orange,cyan,magenta,darkred,darkblue,darkgreen +## Line width scaling (for graphs) +plot.line-width=0.005 +## For the cluster order +plot.clusterorder.line-width=0.002 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.005 +## Greyed out color +plot.grey=grey +## Dot size +plot.dot.size=0.002 +## Reference points +plot.referencepoints.size=0.003 +plot.referencepoints.color=red +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=grey + +## Curve vis (ROC curves) labels: +xycurve.text-size=0.03 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=darkblue +plot.selection.opacity=0.25 +plot.selection.size=0.015 diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/default.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/default.properties new file mode 100644 index 00000000..bfcaaa3a --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/default.properties @@ -0,0 +1,81 @@ +## Libraries +lines-library = SolidLineStyleLibrary +marker-library = PrettyMarkers + +## Default foreground color +color=black +## Default font family +font-family='Times New Roman', serif +## Default background color +background-color=white +## Default line width +line-width=0.01 +## Text font size scale +text-size=0.02 +## Text color +text-color=black +## Default margin (relative) +margin.size=0.1 + +## Named color for the page background +page.background-color=white +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=black +## Named color for a typical axis tick mark +axis.tick.color=grey +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=black +## Axis line width +axis.line-width=0.002 +## Axis tick width +axis.tick.line-width=0.002 +## Axis label font size +axis.label.text-size=0.02 + +## Named color for a label in the key part +key.text-size=0.007 +key.color=black +key.hierarchy.line-width=0.1 + +## A list of color names for data lines +colorset=#ed420e,#fdca19,#4548a5,#7ebd3a,#a81e51,#00748b,#fa8116,#512d85,#008a7a,#fea918,#019d60,#cfde3d,#015a9c,#7b1760 +## Line width scaling (for graphs) +plot.line-width=0.005 +## For the cluster order +plot.clusterorder.line-width=0.002 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.005 +## Greyed out color +plot.grey=grey +## Dot size +plot.dot.size=0.002 +## Reference points +plot.referencepoints.size=0.003 +plot.referencepoints.color=red +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=grey + +## Curve vis (ROC curves) labels: +xycurve.text-size=0.03 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=darkblue +plot.selection.opacity=0.25 +plot.selection.size=0.015 + +## Circle segment colors. Will be interpolated to produce extra classes. +segments.border.color=#FF0073 +segments.hover.color=#73ff00 +segments.cluster.first.color=#505050 +segments.cluster.second.color=#E0E0E0
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/greyscale.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/greyscale.properties new file mode 100644 index 00000000..f27a85a2 --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/greyscale.properties @@ -0,0 +1,79 @@ +## Libraries +lines-library = DashedLineStyleLibrary +marker-library = PrettyMarkers + +## Default foreground color +color=black +## Default font family +font-family='Times New Roman', serif +## Default background color +background-color=white +## Default line width +line-width=0.01 +## Text font size scale +text-size=0.02 +## Text color +text-color=black +## Default margin (relative) +margin.size=0.10 + +## Named color for the page background +page.background-color=white +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=black +## Named color for a typical axis tick mark +axis.tick.color=grey +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=black +## Axis line width +axis.line-width=0.002 +## Axis tick width +axis.tick.line-width=0.002 +## Axis label font size +axis.label.text-size=0.02 + +## Named color for a label in the key part +key.text-size=0.007 +key.color=black +key.hierarchy.line-width=0.05 + +## A list of color names for data lines +# We stick to primary colors first to have the least issues +# The first two colors are red and blue to help red-green blind people. +# Yellow usually offers bad contrast, therefore comes late. +# Magenta often shows up too similar to red, cyan too similar to blue in print. +colorset=black,grey,silver,darkgrey,dimgrey +## Line width scaling (for graphs) +plot.line-width=0.005 +## For the cluster order +plot.clusterorder.line-width=0.002 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.005 +## Greyed out color +plot.grey=lightgrey +## Dot size +plot.dot.size=0.002 +## Reference points +plot.referencepoints.size=0.003 +plot.referencepoints.color=black +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=dimgrey + +## Curve vis (ROC curves) labels: +xycurve.text-size=0.03 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=grey +plot.selection.opacity=0.25 +plot.selection.size=0.015
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/kelly.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/kelly.properties new file mode 100644 index 00000000..808b8161 --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/kelly.properties @@ -0,0 +1,83 @@ +#### Based on Kelly, Kenneth L. "Twenty-two colors of maximum contrast." Color Engineering 3.26 (1965): 26-27. +## Libraries +lines-library = SolidLineStyleLibrary +marker-library = PrettyMarkers + +## Default foreground color +color=black +## Default font family +font-family='Times New Roman', serif +## Default background color +background-color=white +## Default line width +line-width=0.01 +## Text font size scale +text-size=0.02 +## Text color +text-color=black +## Default margin (relative) +margin.size=0.1 + +## Named color for the page background +page.background-color=white +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=black +## Named color for a typical axis tick mark +axis.tick.color=grey +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=black +## Axis line width +axis.line-width=0.002 +## Axis tick width +axis.tick.line-width=0.002 +## Axis label font size +axis.label.text-size=0.02 + +## Named color for a label in the key part +key.text-size=0.007 +key.color=black +key.hierarchy.line-width=0.1 + +## A list of color names for data lines +# Yes, this is just 20. We skipped #F2F3F4,#222222. +colorset=#F3C300,#875692,#F38400,#A1CAF1,#BE0032,#C2B280,#848482,#008856,#E68FAC,#0067A5,#F99379,#604E97,#F6A600,#B3446C,#DCD300,#882D17,#8DB600,#654522,#E25822,#2B3D26 +## Line width scaling (for graphs) +plot.line-width=0.005 +## For the cluster order +plot.clusterorder.line-width=0.002 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.005 +## Greyed out color +plot.grey=grey +## Dot size +plot.dot.size=0.002 +## Reference points +plot.referencepoints.size=0.003 +plot.referencepoints.color=red +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=grey + +## Curve vis (ROC curves) labels: +xycurve.text-size=0.03 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=darkblue +plot.selection.opacity=0.25 +plot.selection.size=0.015 + +## Circle segment colors. Will be interpolated to produce extra classes. +segments.border.color=#FF0073 +segments.hover.color=#73ff00 +segments.cluster.first.color=#505050 +segments.cluster.second.color=#E0E0E0
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/neon.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/neon.properties new file mode 100644 index 00000000..d18c393a --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/neon.properties @@ -0,0 +1,79 @@ +## Libraries +lines-library = SolidLineStyleLibrary +marker-library = PrettyMarkers + +## Default foreground color +color=white +## Default font family +font-family='Times New Roman', serif +## Default background color +background-color=black +## Default line width +line-width=0.01 +## Text font size scale +text-size=0.02 +## Text color +text-color=white +## Default margin (relative) +margin.size=0.10 + +## Named color for the page background +page.background-color=black +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=white +## Named color for a typical axis tick mark +axis.tick.color=grey +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=white +## Axis line width +axis.line-width=0.002 +## Axis tick width +axis.tick.line-width=0.002 +## Axis label font size +axis.label.text-size=0.02 + +## Named color for a label in the key part +key.text-size=0.007 +key.color=white +key.hierarchy.line-width=0.05 + +## A list of color names for data lines +# We stick to primary colors first to have the least issues +# The first two colors are red and blue to help red-green blind people. +# Yellow usually offers bad contrast, therefore comes late. +# Magenta often shows up too similar to red, cyan too similar to blue in print. +colorset=yellow,magenta,cyan,red,blue,green,orange +## Line width scaling (for graphs) +plot.line-width=0.005 +## For the cluster order +plot.clusterorder.line-width=0.002 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.005 +## Greyed out color +plot.grey=darkgrey +## Dot size +plot.dot.size=0.002 +## Reference points +plot.referencepoints.size=0.003 +plot.referencepoints.color=red +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=grey + +## Curve vis (ROC curves) labels: +xycurve.text-size=0.03 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=blue +plot.selection.opacity=0.25 +plot.selection.size=0.015
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/presentation.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/presentation.properties new file mode 100644 index 00000000..d30667b6 --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/presentation.properties @@ -0,0 +1,85 @@ +## Libraries +lines-library = SolidLineStyleLibrary +marker-library = PrettyMarkers + +## Default foreground color +color=black +## Default font family +font-family='CompatilFact', 'Droid Sans', sans-serif +## Default background color +background-color=white +## Default line width +line-width=0.02 +## Text font size scale +text-size=0.015 +## Text color +text-color=black +## Default margin (relative) +margin.size=0.10 + +## Named color for the page background +page.background-color=white +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=black +## Named color for a typical axis tick mark +axis.tick.color=grey +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=black +## Axis line width +axis.line-width=0.004 +## Axis tick width +axis.tick.line-width=0.004 +## Axis label font size +axis.label.text-size=0.03 + +## Named color for a label in the key part +key.text-size=0.01 +key.color=black +key.hierarchy.line-width=0.1 + +## A list of color names for data lines +# We stick to primary colors first to have the least issues +# The first two colors are red and blue to help red-green blind people. +# Yellow usually offers bad contrast, therefore comes late. +# Magenta often shows up too similar to red, cyan too similar to blue in print. +colorset=#ed420e,#fdca19,#4548a5,#7ebd3a,#a81e51,#00748b,#fa8116,#512d85,#008a7a,#fea918,#019d60,#cfde3d,#015a9c,#7b1760 +## Line width scaling (for graphs) +plot.line-width=0.01 +## For the cluster order +plot.clusterorder.line-width=0.004 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.01 +## Greyed out color +plot.grey=grey +## Dot size +plot.dot.size=0.004 +## Reference points +plot.referencepoints.size=0.006 +plot.referencepoints.color=red +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=grey + +## Curve vis (ROC curves) labels: +xycurve.text-size=0.04 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=red +plot.selection.opacity=0.4 +plot.selection.size=0.03 + +## Circle segment colors. Will be interpolated to produce extra classes. +segments.border.color=#FF0073 +segments.hover.color=#73ff00 +segments.cluster.first.color=#404050 +segments.cluster.second.color=#D0D0F0
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/print.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/print.properties new file mode 100644 index 00000000..02c19ba1 --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/print.properties @@ -0,0 +1,85 @@ +## Libraries +lines-library = SolidLineStyleLibrary +marker-library = PrettyMarkers + +## Default foreground color +color=black +## Default font family +font-family='Times New Roman', serif +## Default background color +background-color=white +## Default line width +line-width=0.01 +## Text font size scale +text-size=0.03 +## Text color +text-color=black +## Default margin (relative) +margin.size=0.10 + +## Named color for the page background +page.background-color=white +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=black +## Named color for a typical axis tick mark +axis.tick.color=grey +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=black +## Axis line width +axis.line-width=0.004 +## Axis tick width +axis.tick.line-width=0.004 +## Axis label font size +axis.label.text-size=0.03 + +## Named color for a label in the key part +key.text-size=0.007 +key.color=black +key.hierarchy.line-width=0.05 + +## A list of color names for data lines +# We stick to primary colors first to have the least issues +# The first two colors are red and blue to help red-green blind people. +# Yellow usually offers bad contrast, therefore comes late. +# Magenta often shows up too similar to red, cyan too similar to blue in print. +colorset=black,#ed420e,#fdca19,#4548a5,#7ebd3a,#a81e51,#00748b,#fa8116,#512d85,#008a7a,#fea918,#019d60,#cfde3d,#015a9c,#7b1760 +## Line width scaling (for graphs) +plot.line-width=0.005 +## For the cluster order +plot.clusterorder.line-width=0.003 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.01 +## Greyed out color +plot.grey=grey +## Dot size +plot.dot.size=0.004 +## Reference points +plot.referencepoints.size=0.006 +plot.referencepoints.color=red +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=grey + +## Curve vis (ROC curves) labels: +xycurve.text-size=0.04 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=darkblue +plot.selection.opacity=0.4 +plot.selection.size=0.015 + +## Circle segment colors. Will be interpolated to produce extra classes. +segments.border.color=#FF0073 +segments.hover.color=#73ff00 +segments.cluster.first.color=#404050 +segments.cluster.second.color=#D0D0F0
\ No newline at end of file diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/wikipedia.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/wikipedia.properties new file mode 100644 index 00000000..e02ad985 --- /dev/null +++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/wikipedia.properties @@ -0,0 +1,77 @@ +## Libraries +lines-library = SolidLineStyleLibrary +marker-library = CircleMarkers + +## Default foreground color +color=black +## Default font family +font-family='Times New Roman', serif +## Default background color +background-color=white +## Default line width +line-width=0.01 +## Text font size scale +text-size=0.03 +## Text color +text-color=black +## Default margin (relative) +margin.size=0.1 + +## Named color for the page background +page.background-color=white +## Background color for plot area +plot.background-color=none + +## Named color for a typical axis +axis.color=black +## Named color for a typical axis tick mark +axis.tick.color=gray +## Named color for a typical axis tick mark +axis.tick.minor.color=silver +## Named color for a typical axis label +axis.label.color=black +## Axis line width +axis.line-width=0.002 +## Axis tick width +axis.tick.line-width=0.002 +## Axis label font size +axis.label.text-size=0.02 + +## Named color for a label in the key part +key.text-size=0.007 +key.color=black +key.hierarchy.line-width=0.05 + +## A list of color names for data lines +# We stick to primary colors first to have the least issues +# The first two colors are red and blue to help red-green blind people. +# Yellow usually offers bad contrast, therefore comes late. +# Magenta often shows up too similar to red, cyan too similar to blue in print. +colorset=red,blue,green,orange,cyan,magenta,darkred,darkblue,darkgreen +## Line width scaling (for graphs) +plot.line-width=0.005 +## For the cluster order +plot.clusterorder.line-width=0.002 +## Bubble sizes (relative) +plot.bubble.size=0.10 +## Marker size (relative) +plot.marker.size=0.01 +## Dot size +plot.dot.size=0.005 +## Reference points +plot.referencepoints.size=0.003 +plot.referencepoints.color=red +## Polygons +plot.polygons.line-width=0.001 +plot.polygons.color=gray + +## Curve vis (ROC curves) labels: +curve.labels.text-size=0.04 + +# Text size in overview plot +overview.labels.text-size=0.08 + +## Selection colors +plot.selection.color=darkblue +plot.selection.opacity=0.25 +plot.selection.size=0.015 |