diff options
Diffstat (limited to 'src/de/lmu/ifi/dbs/elki/visualization/gui/overview')
5 files changed, 848 insertions, 404 deletions
diff --git a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java index 0ea59547..085fc262 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java @@ -1,26 +1,27 @@ package de.lmu.ifi.dbs.elki.visualization.gui.overview; + /* -This file is part of ELKI: -Environment for Developing KDD-Applications Supported by Index-Structures + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures -Copyright (C) 2011 -Ludwig-Maximilians-Universität München -Lehr- und Forschungseinheit für Datenbanksysteme -ELKI Development Team + Copyright (C) 2011 + 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 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. + 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/>. -*/ + 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; @@ -44,14 +45,9 @@ public class DetailViewSelectedEvent extends ActionEvent { OverviewPlot overview; /** - * X Coordinate - */ - double x; - - /** - * X Coordinate + * Plot item selected */ - double y; + PlotItem it; /** * Constructor. To be called by OverviewPlot only! @@ -60,14 +56,12 @@ public class DetailViewSelectedEvent extends ActionEvent { * @param id ID * @param command command that was invoked * @param modifiers modifiers - * @param x x click - * @param y y click + * @param it Plot item selected */ - public DetailViewSelectedEvent(OverviewPlot source, int id, String command, int modifiers, double x, double y) { + public DetailViewSelectedEvent(OverviewPlot source, int id, String command, int modifiers, PlotItem it) { super(source, id, command, modifiers); this.overview = source; - this.x = x; - this.y = y; + this.it = it; } /** @@ -76,6 +70,6 @@ public class DetailViewSelectedEvent extends ActionEvent { * @return materialized detail plot */ public DetailView makeDetailView() { - return overview.makeDetailView(x, y); + return overview.makeDetailView(it); } }
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java index 6fd1f0f8..5c8b8d44 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java @@ -1,32 +1,35 @@ 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) 2011 -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/>. -*/ +/* + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures + + Copyright (C) 2011 + 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.HashMap; -import java.util.List; +import java.util.Iterator; +import java.util.Map.Entry; import org.apache.batik.util.SVGConstants; import org.w3c.dom.Element; @@ -34,33 +37,24 @@ 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.data.type.TypeUtil; -import de.lmu.ifi.dbs.elki.database.Database; -import de.lmu.ifi.dbs.elki.database.relation.Relation; +import de.lmu.ifi.dbs.elki.logging.Logging; import de.lmu.ifi.dbs.elki.logging.LoggingUtil; -import de.lmu.ifi.dbs.elki.math.linearalgebra.AffineTransformation; +import de.lmu.ifi.dbs.elki.result.HierarchicalResult; 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.DatabaseUtil; +import de.lmu.ifi.dbs.elki.result.ResultUtil; import de.lmu.ifi.dbs.elki.utilities.pairs.Pair; +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.detail.DetailView; -import de.lmu.ifi.dbs.elki.visualization.projections.AffineProjection; -import de.lmu.ifi.dbs.elki.visualization.projections.Projection1D; -import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D; -import de.lmu.ifi.dbs.elki.visualization.projections.Simple1D; -import de.lmu.ifi.dbs.elki.visualization.projections.Simple2D; -import de.lmu.ifi.dbs.elki.visualization.scales.LinearScale; -import de.lmu.ifi.dbs.elki.visualization.scales.Scales; +import de.lmu.ifi.dbs.elki.visualization.projector.Projector; 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; -import de.lmu.ifi.dbs.elki.visualization.visualizers.VisualizationTask; -import de.lmu.ifi.dbs.elki.visualization.visualizers.VisualizerContext; import de.lmu.ifi.dbs.elki.visualization.visualizers.VisualizerUtil; -import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisFactory; /** * Generate an overview plot for a set of visualizations. @@ -77,16 +71,9 @@ import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisFactory; */ public class OverviewPlot extends SVGPlot implements ResultListener { /** - * Maximum number of dimensions to visualize. - * - * TODO: Erich: add scrolling function for higher dimensionality! + * Our logging class */ - public static final int MAX_DIMENSIONS_DEFAULT = 10; - - /** - * Stores the maximum number of dimensions to show. - */ - private int maxdim = MAX_DIMENSIONS_DEFAULT; + private static final Logging logger = Logging.getLogger(OverviewPlot.class); /** * Visualizer context @@ -94,20 +81,14 @@ public class OverviewPlot extends SVGPlot implements ResultListener { private VisualizerContext context; /** - * Database we work on. - */ - private Database db; - - /** * Result we work on. Currently unused, but kept for future requirements. */ - @SuppressWarnings("unused") - private Result result; + private HierarchicalResult result; /** * Map of coordinates to plots. */ - protected PlotMap<NumberVector<?, ?>> plotmap; + protected RectangleArranger<PlotItem> plotmap; /** * Action listeners for this plot. @@ -117,15 +98,11 @@ public class OverviewPlot extends SVGPlot implements ResultListener { /** * Constructor. * - * @param db Database * @param result Result to visualize - * @param maxdim Maximum number of dimensions * @param context Visualizer context */ - public OverviewPlot(Database db, Result result, int maxdim, VisualizerContext context) { + public OverviewPlot(HierarchicalResult result, VisualizerContext context) { super(); - this.maxdim = maxdim; - this.db = db; this.result = result; this.context = context; // register context listener @@ -173,105 +150,49 @@ public class OverviewPlot extends SVGPlot implements ResultListener { private double ratio = 1.0; /** + * Pending refresh, for lazy refreshing + */ + Runnable pendingRefresh; + + /** + * Reinitialize on refresh + */ + private boolean reinitOnRefresh = true; + + /** * Recompute the layout of visualizations. */ private void arrangeVisualizations() { - // split the visualizers into three sets. - // FIXME: THIS IS VERY UGLY, and needs to be refactored. - // (This is a remainder of merging adapters and visualizationfactories) - List<VisualizationTask> vis = new ArrayList<VisualizationTask>(); - for(VisualizationTask task : context.iterVisualizers()) { - vis.add(task); - } - // We'll use three regions for now: - // 2D projections starting at 0,0 and going right and down. - // 1D projections starting at 0, -1 and going right - // Other projections starting at -1, min() and going down. - plotmap = new PlotMap<NumberVector<?, ?>>(); - // FIXME: ugly cast used here. - Relation<NumberVector<?, ?>> dvdb = db.getRelation(TypeUtil.DOUBLE_VECTOR_FIELD); - LinearScale[] scales = null; - scales = Scales.calcScales(dvdb); - int dmax = Math.min(DatabaseUtil.dimensionality(dvdb), maxdim); - for(int d1 = 1; d1 <= dmax; d1++) { - for(int d2 = d1 + 1; d2 <= dmax; d2++) { - Projection2D proj = new Simple2D(scales, d1, d2); - - for(VisualizationTask task : vis) { - if(task.getFactory().getProjectionType() == Projection2D.class) { - plotmap.addVis(d1 - 1, d2 - 2, 1., 1., proj, task); - } - } + plotmap = new RectangleArranger<PlotItem>(ratio); + + ArrayList<Projector> projectors = ResultUtil.filterResults(result, Projector.class); + // Rectangle layout + for(Projector p : projectors) { + Collection<PlotItem> projs = p.arrange(); + for(PlotItem it : projs) { + plotmap.put(it.w, it.h, it); } } - if(dmax >= 3) { - AffineTransformation p = AffineProjection.axisProjection(DatabaseUtil.dimensionality(dvdb), 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.); - final double sizeh = Math.ceil((dmax - 1) / 2.0); - Projection2D proj = new AffineProjection(scales, p); - for(VisualizationTask task : vis) { - if(task.getFactory().getProjectionType() == Projection2D.class) { - plotmap.addVis(Math.ceil((dmax - 1) / 2.0), 0.0, sizeh, sizeh, proj, task); - } - } - } - // insert column numbers - for(int d1 = 1; d1 <= dmax; d1++) { - VisualizationTask colvi = new VisualizationTask("", context, null, null, new LabelVisFactory(Integer.toString(d1)), null, null, this, 1, .1); - colvi.put(VisualizationTask.META_NODETAIL, true); - plotmap.addVis(d1 - 1, -.1, 1., .1, null, colvi); - } - // insert row numbers - for(int d1 = 2; d1 <= dmax; d1++) { - VisualizationTask colvi = new VisualizationTask("", context, null, null, new LabelVisFactory(Integer.toString(d1)), null, null, this, .1, 1); - colvi.put(VisualizationTask.META_NODETAIL, true); - plotmap.addVis(-.1, d1 - 2, .1, 1., null, colvi); - } - { - int dim = dmax; - for(int d1 = 1; d1 <= dim; d1++) { - Projection1D proj = new Simple1D(scales, d1); - double ypos = -.1; - for(VisualizationTask task : vis) { - if(task.getFactory().getProjectionType() == Projection1D.class) { - // TODO: 1d vis might have a different native scaling. - double height = 0.5; - plotmap.addVis(d1 - 1, ypos - height, 1.0, height, proj, task); - //ypos = ypos - height; - } + + ResultHierarchy hier = result.getHierarchy(); + ArrayList<VisualizationTask> tasks = ResultUtil.filterResults(result, VisualizationTask.class); + for(VisualizationTask task : tasks) { + boolean isprojected = false; + for(Result parent : hier.getParents(task)) { + if(parent instanceof Projector) { + isprojected = true; + break; } } - } - { - HashMap<Object, double[]> stackmap = new HashMap<Object, double[]>(); - // find starting position. - Double pos = plotmap.minmaxy.getMin(); - if(pos == null) { - pos = 0.0; - } - // FIXME: use multiple columns! - for(VisualizationTask task : vis) { - if(task.getFactory().getProjectionType() == Projection1D.class) { - continue; - } - if(task.getFactory().getProjectionType() == Projection2D.class) { - continue; - } - double[] p = null; - if(task.getVisualizationStack() != null) { - p = stackmap.get(task.getVisualizationStack()); + if(!isprojected) { + if(task.getWidth() <= 0.0 || task.getHeight() <= 0.0) { + logger.warning("Task with improper size information: " + task); } - if(p == null) { - p = new double[] { -1.1, pos }; - pos += 1.0; - stackmap.put(task.getVisualizationStack(), p); + else { + PlotItem it = new PlotItem(task.getWidth(), task.getHeight(), null); + it.visualizations.add(task); + plotmap.put(it.w, it.h, it); } - // TODO: might have different scaling preferences - plotmap.addVis(p[0], p[1], 1., 1., null, task); } } } @@ -279,7 +200,7 @@ public class OverviewPlot extends SVGPlot implements ResultListener { /** * Refresh the overview plot. */ - public void reinitialize() { + private void reinitialize() { setupHoverer(); arrangeVisualizations(); recalcViewbox(); @@ -305,32 +226,37 @@ public class OverviewPlot extends SVGPlot implements ResultListener { final int thumbsize = (int) Math.max(screenwidth / plotmap.getWidth(), screenheight / plotmap.getHeight()); // TODO: kill all children in document root except style, defs etc? - for(PlotItem it : plotmap.values()) { - boolean hasDetails = false; - Element g = this.svgElement(SVGConstants.SVG_G_TAG); - SVGUtil.setAtt(g, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "translate(" + it.x + " " + it.y + ")"); - for(VisualizationTask task : it) { - Element parent = this.svgElement(SVGConstants.SVG_G_TAG); - g.appendChild(parent); - makeThumbnail(thumbsize, it, task, parent); - vistoelem.put(new Pair<PlotItem, VisualizationTask>(it, task), parent); - - if(VisualizerUtil.detailsEnabled(task)) { - hasDetails = true; + 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; + Element g = this.svgElement(SVGConstants.SVG_G_TAG); + SVGUtil.setAtt(g, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "translate(" + (basex + it.x) + " " + (basey + it.y) + ")"); + for(VisualizationTask task : it.visualizations) { + Element parent = this.svgElement(SVGConstants.SVG_G_TAG); + g.appendChild(parent); + makeThumbnail(thumbsize, it, task, parent); + vistoelem.put(new Pair<PlotItem, VisualizationTask>(it, task), parent); + + if(VisualizerUtil.detailsEnabled(task)) { + hasDetails = true; + } + } + plotlayer.appendChild(g); + if(hasDetails) { + Element hover = this.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); } - } - plotlayer.appendChild(g); - if(hasDetails) { - Element hover = this.svgRect(it.x, 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.x, it.y), false); - - hoverlayer.appendChild(hover); } } getRoot().appendChild(plotlayer); @@ -364,38 +290,47 @@ public class OverviewPlot extends SVGPlot implements ResultListener { /** * Do a refresh (when visibilities have changed). */ - public void refresh() { - if(vistoelem == null || plotlayer == null || hoverlayer == null) { + synchronized void refresh() { + logger.debug("Refresh"); + if(vistoelem == null || plotlayer == null || hoverlayer == null || reinitOnRefresh) { reinitialize(); + reinitOnRefresh = false; } else { + boolean refreshcss = false; final int thumbsize = (int) Math.max(screenwidth / plotmap.getWidth(), screenheight / plotmap.getHeight()); - for(PlotItem it : plotmap.values()) { - for(VisualizationTask task : it) { - Element gg = vistoelem.get(new Pair<PlotItem, VisualizationTask>(it, task)); - if(gg == null) { - LoggingUtil.warning("No container element found for " + task); + for(Entry<PlotItem, double[]> ent : plotmap.entrySet()) { + PlotItem it = ent.getKey(); + for(Iterator<VisualizationTask> iter = it.visIterator(); iter.hasNext(); ) { + VisualizationTask task = iter.next(); + Element parent = vistoelem.get(new Pair<PlotItem, VisualizationTask>(it, task)); + if(parent == null) { + LoggingUtil.warning("No container element produced by " + task); continue; } if(VisualizerUtil.thumbnailEnabled(task) && VisualizerUtil.isVisible(task)) { // unhide when hidden. - if(gg.hasAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)) { - gg.removeAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY); + if(parent.hasAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)) { + parent.removeAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY); } // if not yet rendered, add a thumbnail - if(!gg.hasChildNodes()) { - makeThumbnail(thumbsize, it, task, gg); + if(!parent.hasChildNodes()) { + makeThumbnail(thumbsize, it, task, parent); + refreshcss = true; } } else { // hide if there is anything to hide. - if(gg != null && gg.hasChildNodes()) { - gg.setAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY, SVGConstants.CSS_HIDDEN_VALUE); + if(parent != null && parent.hasChildNodes()) { + parent.setAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY, SVGConstants.CSS_HIDDEN_VALUE); } // TODO: unqueue pending thumbnails } } } + if(refreshcss) { + updateStyleElement(); + } } } @@ -404,7 +339,7 @@ public class OverviewPlot extends SVGPlot implements ResultListener { */ private void recalcViewbox() { // Recalculate bounding box. - String vb = plotmap.minmaxx.getMin() + " " + plotmap.minmaxy.getMin() + " " + plotmap.getWidth() + " " + plotmap.getHeight(); + String vb = "0 0 " + plotmap.getWidth() + " " + plotmap.getHeight(); // Reset root bounding box. SVGUtil.setAtt(getRoot(), SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm"); SVGUtil.setAtt(getRoot(), SVGConstants.SVG_HEIGHT_ATTRIBUTE, (20 / plotmap.getWidth() * plotmap.getHeight()) + "cm"); @@ -432,13 +367,11 @@ public class OverviewPlot extends SVGPlot implements ResultListener { /** * Event triggered when a plot was selected. * - * @param x X coordinate - * @param y Y coordinate + * @param it Plot item selected * @return sub plot */ - public DetailView makeDetailView(double x, double y) { - PlotItem layers = plotmap.get(x, y); - return new DetailView(context, layers, ratio); + public DetailView makeDetailView(PlotItem it) { + return new DetailView(context, it, ratio); } /** @@ -453,13 +386,12 @@ public class OverviewPlot extends SVGPlot implements ResultListener { /** * When a subplot was selected, forward the event to listeners. * - * @param x X coordinate - * @param y Y coordinate + * @param it PlotItem selected */ - protected void triggerSubplotSelectEvent(double x, double y) { + protected void triggerSubplotSelectEvent(PlotItem it) { // forward event to all listeners. for(ActionListener actionListener : actionListeners) { - actionListener.actionPerformed(new DetailViewSelectedEvent(this, ActionEvent.ACTION_PERFORMED, null, 0, x, y)); + actionListener.actionPerformed(new DetailViewSelectedEvent(this, ActionEvent.ACTION_PERFORMED, null, 0, it)); } } @@ -472,30 +404,23 @@ public class OverviewPlot extends SVGPlot implements ResultListener { */ public class SelectPlotEvent implements EventListener { /** - * X coordinate of box. - */ - double x; - - /** - * Y coordinate of box. + * Plot item clicked */ - double y; + PlotItem it; /** * Constructor. * - * @param x coordinate - * @param y coordinate + * @param it Item that was clicked */ - public SelectPlotEvent(double x, double y) { + public SelectPlotEvent(PlotItem it) { super(); - this.x = x; - this.y = y; + this.it = it; } @Override public void handleEvent(@SuppressWarnings("unused") Event evt) { - triggerSubplotSelectEvent(x, y); + triggerSubplotSelectEvent(it); } } @@ -522,27 +447,43 @@ public class OverviewPlot extends SVGPlot implements ResultListener { this.ratio = ratio; } + /** + * Trigger a redraw, but avoid excessive redraws. + */ + public final void lazyRefresh() { + Runnable pr = new Runnable() { + @Override + public void run() { + if(OverviewPlot.this.pendingRefresh == this) { + OverviewPlot.this.pendingRefresh = null; + OverviewPlot.this.refresh(); + } + } + }; + pendingRefresh = pr; + scheduleUpdate(pr); + } + @SuppressWarnings("unused") @Override public void resultAdded(Result child, Result parent) { - // TODO: be lazy - if (child instanceof VisualizationTask) { - reinitialize(); + logger.debug("result added: " + child); + if(child instanceof VisualizationTask) { + reinitOnRefresh = true; } - refresh(); + lazyRefresh(); } - @SuppressWarnings("unused") @Override public void resultChanged(Result current) { - // TODO: be lazy - refresh(); + logger.debug("result changed: " + current); + lazyRefresh(); } @SuppressWarnings("unused") @Override public void resultRemoved(Result child, Result parent) { - // TODO: be lazy - refresh(); + logger.debug("result removed: " + child); + lazyRefresh(); } }
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java index 3d58d10d..cd8f5b3e 100644 --- a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java +++ b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java @@ -1,31 +1,36 @@ package de.lmu.ifi.dbs.elki.visualization.gui.overview; + /* -This file is part of ELKI: -Environment for Developing KDD-Applications Supported by Index-Structures + This file is part of ELKI: + Environment for Developing KDD-Applications Supported by Index-Structures -Copyright (C) 2011 -Ludwig-Maximilians-Universität München -Lehr- und Forschungseinheit für Datenbanksysteme -ELKI Development Team + Copyright (C) 2011 + 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 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. + 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/>. -*/ + 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; -import de.lmu.ifi.dbs.elki.visualization.visualizers.VisualizationTask; /** * Item to collect visualization tasks on a specific position on the plot map. @@ -36,12 +41,7 @@ import de.lmu.ifi.dbs.elki.visualization.visualizers.VisualizationTask; * * @apiviz.composedOf Projection */ -public class PlotItem extends LinkedList<VisualizationTask> { - /** - * Serial version - */ - private static final long serialVersionUID = 1L; - +public class PlotItem { /** * Position: x */ @@ -66,7 +66,28 @@ public class PlotItem extends LinkedList<VisualizationTask> { * Projection (may be {@code null}!) */ public final Projection proj; - + + /** + * The visualizations at this location + */ + public List<VisualizationTask> visualizations = new LinkedList<VisualizationTask>(); + + /** + * Subitems to plot + */ + public Collection<PlotItem> subitems = new LinkedList<PlotItem>(); + + /** + * 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. * @@ -85,9 +106,130 @@ public class PlotItem extends LinkedList<VisualizationTask> { this.proj = proj; } - @Override - public int hashCode() { - // We can't have our hashcode change with the list contents! - return System.identityHashCode(this); + /** + * Sort all visualizers for their proper drawing order + */ + public void sort() { + Collections.sort(visualizations); + for(PlotItem subitem : subitems) { + subitem.sort(); + } + } + + /** + * Iterate (recursively) over all visualizations. + * + * @return Iterator + */ + public Iterator<VisualizationTask> visIterator() { + return new VisItr(); + } + + /** + * Iterate (recursively) over all plot items, including itself. + * + * @return Iterator + */ + public Iterator<PlotItem> itemIterator() { + return new ItmItr(); + } + + /** + * Recursive iterator + * + * @author Erich Schubert + * + * @apiviz.exclude + */ + private class VisItr implements Iterator<VisualizationTask> { + Iterator<VisualizationTask> cur; + + Iterator<PlotItem> sub; + + /** + * Constructor. + */ + public VisItr() { + super(); + this.cur = visualizations.iterator(); + this.sub = subitems.iterator(); + } + + @Override + public boolean hasNext() { + if(cur.hasNext()) { + return true; + } + if(sub.hasNext()) { + cur = sub.next().visIterator(); + return hasNext(); + } + return false; + } + + @Override + public VisualizationTask next() { + hasNext(); + return cur.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /** + * 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/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotMap.java b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotMap.java deleted file mode 100644 index 40d67c06..00000000 --- a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotMap.java +++ /dev/null @@ -1,124 +0,0 @@ -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) 2011 -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 de.lmu.ifi.dbs.elki.logging.LoggingUtil; -import de.lmu.ifi.dbs.elki.math.DoubleMinMax; -import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleDoublePair; -import de.lmu.ifi.dbs.elki.visualization.projections.Projection; -import de.lmu.ifi.dbs.elki.visualization.visualizers.VisualizationTask; - -/** - * Manage the Overview plot canvas. - * - * @author Erich Schubert - * - * @apiviz.composedOf PlotItem - */ -class PlotMap<NV> extends HashMap<DoubleDoublePair, PlotItem> { - /** - * Serial version - */ - private static final long serialVersionUID = 1L; - - /** - * X coordinates seen - */ - DoubleMinMax minmaxx = new DoubleMinMax(); - - /** - * Y coordinates seen - */ - DoubleMinMax minmaxy = new DoubleMinMax(); - - /** - * Constructor. - */ - PlotMap() { - super(); - } - - /** - * Place a new visualization on the chart. - * - * @param x X coordinate - * @param y Y coordinate - * @param w Width - * @param h Height - * @param v Visualization - */ - void addVis(double x, double y, double w, double h, Projection proj, VisualizationTask v) { - final DoubleDoublePair pos = new DoubleDoublePair(x, y); - PlotItem l = this.get(pos); - if(l == null) { - l = new PlotItem(x, y, w, h, proj); - this.put(pos, l); - } - else { - // Sanity check - if(l.w != w || l.h != h) { - LoggingUtil.warning("Layout error - different object sizes at the same map position!"); - } - if(l.proj != proj) { - LoggingUtil.warning("Layout error - two different projections used at the same map position."); - } - } - l.add(v); - // Update min/max - minmaxx.put(x); - minmaxx.put(x + w); - minmaxy.put(y); - minmaxy.put(y + h); - } - - /** - * Get the visualization on the given coordinates. - * - * @param x First coordinate - * @param y Second coordinate - * @return Visualizations at this position. - */ - PlotItem get(double x, double y) { - return this.get(new DoubleDoublePair(x, y)); - } - - /** - * Get width in plot units - * - * @return width - */ - public double getWidth() { - return minmaxx.getMax() - minmaxx.getMin(); - } - - /** - * Get height in plot units. - * - * @return height - */ - public double getHeight() { - return minmaxy.getMax() - minmaxy.getMin(); - } -}
\ No newline at end of file diff --git a/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java new file mode 100644 index 00000000..ad87c3fc --- /dev/null +++ b/src/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java @@ -0,0 +1,491 @@ +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) 2011 + 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; + +/** + * 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 logger = 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 ArrayList<Double> widths = new ArrayList<Double>(); + + /** + * Column heights + */ + private ArrayList<Double> heights = new ArrayList<Double>(); + + /** + * Bit sets to store usage. ArrayList = y, BitSet = x + */ + private ArrayList<ArrayList<Object>> usage = new ArrayList<ArrayList<Object>>(); + + /** + * Data + */ + private Map<T, double[]> map = new HashMap<T, double[]>(); + + /** + * 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<Object>(); + 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) { + logger.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); + + logger.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(); + } + logger.debugFinest("Best: " + bestsx + "," + bestsy + " - " + bestex + "," + bestey + " inc: " + bestwi + "x" + besthi + " " + bestinc); + // Need to split a column. + // TODO: find best column to split. Currently: last + if(bestwi < 0) { + splitCol(bestex, -bestwi); + bestwi = 0.0; + } + // Need to split a row. + // TODO: find best row to split. Currently: last + if(besthi < 0) { + splitRow(bestey, -besthi); + besthi = 0.0; + } + // 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; + } + 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(logger.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) { + logger.debugFine("Split row " + bestey); + heights.add(bestey + 1, besthi); + heights.set(bestey, heights.get(bestey) - besthi); + // Update used map + usage.add(bestey + 1, new ArrayList<Object>(usage.get(bestey))); + } + + protected void splitCol(int bestex, double bestwi) { + final int rows = heights.size(); + logger.debugFine("Split column " + bestex); + widths.add(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(); + logger.debugFine("Resize by " + inc + "x" + (inc / ratio)); + if(logger.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<Object>(); + for(int x = 0; x <= cols; x++) { + row.add(null); + } + usage.add(row); + } + assert assertConsistent(); + if(logger.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); + 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); + 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; + } + + public void logSizes() { + StringBuffer buf = new StringBuffer(); + 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"); + } + logger.debug(buf); + } + + /** + * 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()); + } + + /** + * Test method. + * + * @param args + */ + public static void main(String[] args) { + logger.getWrappedLogger().setLevel(Level.FINEST); + RectangleArranger<String> r = new RectangleArranger<String>(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<String>(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<String>(4 - 2.6521739130434785); + r.put(4., .5, "A"); + r.put(4., 3., "B"); + r.put(4., 1., "C"); + r.put(1., .1, "D"); + } +} |