diff options
Diffstat (limited to 'src/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java')
-rw-r--r-- | src/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java | 323 |
1 files changed, 182 insertions, 141 deletions
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 c4825b1e..34471bdd 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 @@ -4,7 +4,7 @@ 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) 2013 + Copyright (C) 2014 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team @@ -29,6 +29,7 @@ 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; @@ -70,20 +71,28 @@ import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; * @apiviz.has DetailViewSelectedEvent * @apiviz.uses DetailView */ -// FIXME: there still is a synchronization issue, that causes the initialization -// to be run twice in parallel. -public class OverviewPlot extends SVGPlot implements ResultListener { +public class OverviewPlot implements ResultListener { /** * Our logging class */ private static final Logging LOG = Logging.getLogger(OverviewPlot.class); /** + * Event when the overview plot was refreshed. + */ + public static final String OVERVIEW_REFRESHED = "Overview refreshed"; + + /** * Visualizer context */ private VisualizerContext context; /** + * The SVG plot object. + */ + private SVGPlot plot; + + /** * Map of coordinates to plots. */ protected RectangleArranger<PlotItem> plotmap; @@ -141,7 +150,7 @@ public class OverviewPlot extends SVGPlot implements ResultListener { /** * Pending refresh, for lazy refreshing */ - Runnable pendingRefresh = null; + AtomicReference<Runnable> pendingRefresh = new AtomicReference<>(null); /** * Reinitialize on refresh @@ -160,26 +169,6 @@ public class OverviewPlot extends SVGPlot implements ResultListener { this.context = context; this.single = single; - // Add a background element: - { - CSSClass cls = new CSSClass(this, "background"); - cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, context.getStyleResult().getStyleLibrary().getBackgroundColor(StyleLibrary.PAGE)); - addCSSClassOrLogError(cls); - Element background = this.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()); - getRoot().appendChild(background); - } - - if (single) { - setDisableInteractions(true); - } - SVGEffects.addShadowFilter(this); - SVGEffects.addLightGradient(this); - // register context listener context.addResultListener(this); } @@ -196,47 +185,57 @@ public class OverviewPlot extends SVGPlot implements ResultListener { ArrayList<Projector> projectors = ResultUtil.filterResults(context.getResult(), Projector.class); // Rectangle layout - for (Projector p : projectors) { + for(Projector p : projectors) { Collection<PlotItem> projs = p.arrange(); - for (PlotItem it : projs) { - if (it.w <= 0.0 || it.h <= 0.0) { + for(PlotItem it : projs) { + if(it.w <= 0.0 || it.h <= 0.0) { LOG.warning("Plot item with improper size information: " + it); - } else { - plotmap.put(it.w, it.h, it); + continue; } + plotmap.put(it.w, it.h, it); } } ResultHierarchy hier = context.getHierarchy(); ArrayList<VisualizationTask> tasks = ResultUtil.filterResults(context.getResult(), VisualizationTask.class); - nextTask: for (VisualizationTask task : tasks) { - if (!task.visible) { + nextTask: for(VisualizationTask task : tasks) { + if(!task.visible) { continue; } - for (Hierarchy.Iter<Result> iter = hier.iterParents(task); iter.valid(); iter.advance()) { - if (iter.get() instanceof Projector) { + for(Hierarchy.Iter<Result> iter = hier.iterParents(task); iter.valid(); iter.advance()) { + if(iter.get() instanceof Projector) { continue nextTask; } } - if (task.getWidth() <= 0.0 || task.getHeight() <= 0.0) { + if(task.getWidth() <= 0.0 || task.getHeight() <= 0.0) { LOG.warning("Task with improper size information: " + task); - } else { - PlotItem it = new PlotItem(task.getWidth(), task.getHeight(), null); - it.tasks.add(task); - plotmap.put(it.w, it.h, it); + continue; } + PlotItem it = new PlotItem(task.getWidth(), task.getHeight(), 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) { + this.ratio = ratio; + reinitialize(); + } + + /** * Refresh the overview plot. */ private void reinitialize() { - setupHoverer(); + initializePlot(); plotmap = arrangeVisualizations(ratio, 1.0); double s = plotmap.relativeFill(); - if (s < 0.9) { + if(s < 0.9) { // Retry, sometimes this yields better results plotmap = arrangeVisualizations(plotmap.getWidth() * s, plotmap.getHeight() * s); } @@ -246,7 +245,7 @@ public class OverviewPlot extends SVGPlot implements ResultListener { // TODO: cancel pending thumbnail requests! // Detach existing elements: - for (Pair<Element, Visualization> pair : vistoelem.values()) { + for(Pair<Element, Visualization> pair : vistoelem.values()) { SVGUtil.removeFromParent(pair.first); } // Replace the layer map @@ -254,44 +253,42 @@ public class OverviewPlot extends SVGPlot implements ResultListener { vistoelem = new LayerMap(); // Redo main layers - SVGUtil.removeFromParent(plotlayer); - SVGUtil.removeFromParent(hoverlayer); - plotlayer = this.svgElement(SVGConstants.SVG_G_TAG); - hoverlayer = this.svgElement(SVGConstants.SVG_G_TAG); + plotlayer = plot.svgElement(SVGConstants.SVG_G_TAG); + hoverlayer = plot.svgElement(SVGConstants.SVG_G_TAG); // Redo the layout - for (Entry<PlotItem, double[]> e : plotmap.entrySet()) { + 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();) { + for(Iterator<PlotItem> iter = e.getKey().itemIterator(); iter.hasNext();) { PlotItem it = iter.next(); boolean hasDetails = false; // Container element for main plot item - Element g = this.svgElement(SVGConstants.SVG_G_TAG); + 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)) { + for(VisualizationTask task : it.tasks) { + if(!visibleInOverview(task)) { continue; } hasDetails |= !task.nodetail; Pair<Element, Visualization> pair = oldlayers.remove(it, task); - if (pair == null) { + if(pair == null) { pair = new Pair<>(null, null); - pair.first = svgElement(SVGConstants.SVG_G_TAG); + pair.first = plot.svgElement(SVGConstants.SVG_G_TAG); } - if (pair.second == null) { + 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 = this.svgRect(basex + it.x, basey + it.y, it.w, it.h); + 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; @@ -304,14 +301,66 @@ public class OverviewPlot extends SVGPlot implements ResultListener { } } } - for (Pair<Element, Visualization> pair : oldlayers.values()) { - if (pair.second != null) { + for(Pair<Element, Visualization> pair : oldlayers.values()) { + if(pair.second != null) { pair.second.destroy(); } } - getRoot().appendChild(plotlayer); - getRoot().appendChild(hoverlayer); - updateStyleElement(); + plot.getRoot().appendChild(plotlayer); + plot.getRoot().appendChild(hoverlayer); + plot.updateStyleElement(); + + // Notify listeners. + for(ActionListener actionListener : actionListeners) { + actionListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, OVERVIEW_REFRESHED)); + } + } + + /** + * Initialize the SVG plot. + */ + private void initializePlot() { + if(plot != null) { + plot.dispose(); + } + plot = new SVGPlot(); + { // Add a background element: + CSSClass cls = new CSSClass(this, "background"); + final String bgcol = context.getStyleResult().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"); + selcss.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE); + selcss.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_NONE_VALUE); + selcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0"); + selcss.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); + CSSClass hovcss = new CSSClass(this, "h"); + hovcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0.25"); + plot.addCSSClassOrLogError(selcss); + plot.addCSSClassOrLogError(hovcss); + // Hover listener. + hoverer = new CSSHoverClass(hovcss.getName(), null, true); + } + + if(single) { + plot.setDisableInteractions(true); + } + SVGEffects.addShadowFilter(plot); + SVGEffects.addLightGradient(plot); + plot.updateStyleElement(); } /** @@ -323,71 +372,70 @@ public class OverviewPlot extends SVGPlot implements ResultListener { * @param parent Parent element to draw to */ private Visualization embedOrThumbnail(final int thumbsize, PlotItem it, VisualizationTask task, Element parent) { - if (single) { - VisualizationTask thumbtask = task.clone(this, context, it.proj, it.w, it.h); - final Visualization vis = thumbtask.getFactory().makeVisualization(thumbtask); - if (vis.getLayer() == null) { - LoggingUtil.warning("Visualization returned empty layer: " + vis); - } else { - if (task.noexport) { - vis.getLayer().setAttribute(NO_EXPORT_ATTRIBUTE, NO_EXPORT_ATTRIBUTE); - } - parent.appendChild(vis.getLayer()); - } - return vis; - } else { - VisualizationTask thumbtask = task.clone(this, context, it.proj, it.w, it.h); + VisualizationTask thumbtask = task.clone(plot, context, it.proj, it.w, it.h); + final Visualization vis; + if(!single) { thumbtask.thumbnail = true; thumbtask.thumbsize = thumbsize; - final Visualization vis = thumbtask.getFactory().makeVisualizationOrThumbnail(thumbtask); - if (vis.getLayer() == null) { - LoggingUtil.warning("Visualization returned empty layer: " + vis); - } else { - parent.appendChild(vis.getLayer()); - } + vis = thumbtask.getFactory().makeVisualizationOrThumbnail(thumbtask); + } + else { + vis = thumbtask.getFactory().makeVisualization(thumbtask); + } + if(vis.getLayer() == null) { + LoggingUtil.warning("Visualization returned empty layer: " + vis); return vis; } + if(task.noexport) { + 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() { - pendingRefresh = null; - if (reinitOnRefresh) { - LOG.debug("Reinitialize"); + void refresh() { + pendingRefresh.set(null); // Clear + if(reinitOnRefresh) { + LOG.debug("Reinitialize in thread " + Thread.currentThread().getName()); reinitialize(); reinitOnRefresh = false; - } else { + return; + } + synchronized(plot) { LOG.debug("Incremental refresh"); boolean refreshcss = false; 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();) { + 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();) { + 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)) { + if(pair == null) { + if(visibleInOverview(task)) { pair = new Pair<>(null, null); - pair.first = svgElement(SVGConstants.SVG_G_TAG); + 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)) { + } + else { + if(visibleInOverview(task)) { // unhide if hidden. - if (pair.first.hasAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)) { + if(pair.first.hasAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)) { pair.first.removeAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY); } - } else { + } + else { // hide if there is anything to hide. - if (pair.first != null && pair.first.hasChildNodes()) { + if(pair.first != null && pair.first.hasChildNodes()) { pair.first.setAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY, SVGConstants.CSS_HIDDEN_VALUE); } } @@ -396,8 +444,8 @@ public class OverviewPlot extends SVGPlot implements ResultListener { } } } - if (refreshcss) { - updateStyleElement(); + if(refreshcss) { + plot.updateStyleElement(); } } } @@ -409,9 +457,10 @@ public class OverviewPlot extends SVGPlot implements ResultListener { * @return visibility */ protected boolean visibleInOverview(VisualizationTask task) { - if (single) { + if(single) { return task.visible && !task.noembed; - } else { + } + else { return task.visible && task.thumbnail; } } @@ -423,28 +472,9 @@ public class OverviewPlot extends SVGPlot implements ResultListener { // Recalculate bounding box. 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"); - SVGUtil.setAtt(getRoot(), SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, vb); - } - - /** - * Setup the CSS hover effect. - */ - private void setupHoverer() { - // setup the hover CSS classes. - selcss = new CSSClass(this, "s"); - selcss.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE); - selcss.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_NONE_VALUE); - selcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0"); - selcss.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE); - CSSClass hovcss = new CSSClass(this, "h"); - hovcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0.25"); - addCSSClassOrLogError(selcss); - addCSSClassOrLogError(hovcss); - // Hover listener. - hoverer = new CSSHoverClass(hovcss.getName(), null, true); - updateStyleElement(); + SVGUtil.setAtt(plot.getRoot(), SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm"); + SVGUtil.setAtt(plot.getRoot(), SVGConstants.SVG_HEIGHT_ATTRIBUTE, (20 * plotmap.getHeight() / plotmap.getWidth()) + "cm"); + SVGUtil.setAtt(plot.getRoot(), SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, vb); } /** @@ -473,7 +503,7 @@ public class OverviewPlot extends SVGPlot implements ResultListener { */ protected void triggerSubplotSelectEvent(PlotItem it) { // forward event to all listeners. - for (ActionListener actionListener : actionListeners) { + for(ActionListener actionListener : actionListeners) { actionListener.actionPerformed(new DetailViewSelectedEvent(this, ActionEvent.ACTION_PERFORMED, null, 0, it)); } } @@ -508,12 +538,20 @@ public class OverviewPlot extends SVGPlot implements ResultListener { } /** - * Cancel the overview, i.e. stop the thumbnailer + * Destroy this overview plot. */ - @Override - public void dispose() { + public void destroy() { context.removeResultListener(this); - super.dispose(); + plot.dispose(); + } + + /** + * Get the SVGPlot object. + * + * @return SVG plot + */ + public SVGPlot getPlot() { + return plot; } /** @@ -527,30 +565,37 @@ public class OverviewPlot extends SVGPlot implements ResultListener { * @param ratio the ratio to set */ public void setRatio(double ratio) { - this.ratio = 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 == this) { + if(OverviewPlot.this.pendingRefresh.compareAndSet(this, null)) { OverviewPlot.this.refresh(); } } }; - pendingRefresh = pr; - scheduleUpdate(pr); + OverviewPlot.this.pendingRefresh.set(pr); + plot.scheduleUpdate(pr); } @Override public void resultAdded(Result child, Result parent) { LOG.debug("result added: " + child); - if (child instanceof VisualizationTask) { + if(child instanceof VisualizationTask) { reinitOnRefresh = true; } lazyRefresh(); @@ -559,17 +604,13 @@ public class OverviewPlot extends SVGPlot implements ResultListener { @Override public void resultChanged(Result current) { LOG.debug("result changed: " + current); - if (current instanceof VisualizationTask) { - boolean relayout = true; - for (Hierarchy.Iter<Result> iter = context.getHierarchy().iterParents(current); iter.valid(); iter.advance()) { - if (iter.get() instanceof Projector) { - relayout = false; + if(current instanceof VisualizationTask) { + for(Hierarchy.Iter<Result> iter = context.getHierarchy().iterParents(current); iter.valid(); iter.advance()) { + if(iter.get() instanceof Projector) { + reinitOnRefresh = true; break; } } - if (relayout) { - reinitOnRefresh = true; - } } lazyRefresh(); } |