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) 2012 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import java.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 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.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.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.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; import de.lmu.ifi.dbs.elki.visualization.visualizers.VisualizerUtil; /** * Generate an overview plot for a set of visualizations. * * @author Erich Schubert * @author Remigius Wojdanowski * * @apiviz.landmark * @apiviz.has VisualizerContext * @apiviz.composedOf RectangleArranger * @apiviz.has DetailViewSelectedEvent * @apiviz.uses DetailView * @apiviz.uses Projector */ // FIXME: there still is a synchronization issue, that causes the initialization // to be run twice in parallel. public class OverviewPlot extends SVGPlot implements ResultListener { /** * Our logging class */ private static final Logging logger = Logging.getLogger(OverviewPlot.class); /** * Visualizer context */ private VisualizerContext context; /** * Map of coordinates to plots. */ protected RectangleArranger plotmap; /** * Action listeners for this plot. */ private java.util.Vector actionListeners = new java.util.Vector(); /** * 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 */ Runnable pendingRefresh = null; /** * Reinitialize on refresh */ private boolean reinitOnRefresh = true; /** * Constructor. * * @param result Result to visualize * @param context Visualizer context * @param single Single view mode */ public OverviewPlot(HierarchicalResult result, VisualizerContext context, boolean single) { super(); this.context = context; this.single = single; // Add a background element: { CSSClass cls = new CSSClass(this, "background"); cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, context.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); } /** * Recompute the layout of visualizations. */ private void arrangeVisualizations() { RectangleArranger plotmap = new RectangleArranger(ratio); ArrayList projectors = ResultUtil.filterResults(context.getResult(), Projector.class); // Rectangle layout for(Projector p : projectors) { Collection projs = p.arrange(); for(PlotItem it : projs) { if(it.w <= 0.0 || it.h <= 0.0) { logger.warning("Plot item with improper size information: " + it); } else { plotmap.put(it.w, it.h, it); } } } ResultHierarchy hier = context.getHierarchy(); ArrayList tasks = ResultUtil.filterResults(context.getResult(), VisualizationTask.class); nextTask: for(VisualizationTask task : tasks) { for(Result parent : hier.getParents(task)) { if(parent instanceof Projector) { continue nextTask; } } if(task.getWidth() <= 0.0 || task.getHeight() <= 0.0) { logger.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); } } this.plotmap = plotmap; } /** * Refresh the overview plot. */ private void reinitialize() { setupHoverer(); arrangeVisualizations(); recalcViewbox(); final int thumbsize = (int) Math.max(screenwidth / plotmap.getWidth(), screenheight / plotmap.getHeight()); // TODO: cancel pending thumbnail requests! // Detach existing elements: for(Pair pair : vistoelem.values()) { SVGUtil.removeFromParent(pair.first); } // Replace the layer map LayerMap oldlayers = vistoelem; 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); // Redo the layout for(Entry e : plotmap.entrySet()) { final double basex = e.getValue()[0]; final double basey = e.getValue()[1]; for(Iterator 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); 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; } if(VisualizerUtil.detailsEnabled(task)) { hasDetails = true; // TODO: not updatable } Pair pair = oldlayers.remove(it, task); if(pair == null) { pair = new Pair(null, null); pair.first = 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 = 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); } } } for(Pair pair : oldlayers.values()) { if(pair.second != null) { pair.second.destroy(); } } getRoot().appendChild(plotlayer); getRoot().appendChild(hoverlayer); updateStyleElement(); } /** * 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) { 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 { parent.appendChild(vis.getLayer()); } return vis; } else { VisualizationTask thumbtask = task.clone(this, context, it.proj, it.w, it.h); thumbtask.put(VisualizationTask.THUMBNAIL, true); thumbtask.put(VisualizationTask.THUMBNAIL_RESOLUTION, thumbsize); final Visualization vis = thumbtask.getFactory().makeVisualizationOrThumbnail(thumbtask); if(vis.getLayer() == null) { LoggingUtil.warning("Visualization returned empty layer: " + vis); } else { parent.appendChild(vis.getLayer()); } return vis; } } /** * Do a refresh (when visibilities have changed). */ synchronized void refresh() { pendingRefresh = null; if(reinitOnRefresh) { logger.debug("Reinitialize"); reinitialize(); reinitOnRefresh = false; } else { logger.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 iter = pi.itemIterator(); iter.hasNext();) { PlotItem it = iter.next(); for(Iterator tit = it.tasks.iterator(); tit.hasNext();) { VisualizationTask task = tit.next(); Pair pair = vistoelem.get(it, task); // New task? if(pair == null) { if(visibleInOverview(task)) { pair = new Pair(null, null); pair.first = 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); } // if not yet rendered, add a thumbnail if(!pair.first.hasChildNodes()) { logger.warning("This codepath should no longer be needed."); Visualization visualization = embedOrThumbnail(thumbsize, it, task, pair.first); vistoelem.put(it, task, pair.first, visualization); refreshcss = true; } } 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) { 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(!VisualizerUtil.isVisible(task)) { return false; } if(single) { Boolean nothumb = task.getGenerics(VisualizationTask.META_NOEMBED, Boolean.class); return (nothumb == null) || !nothumb; } else { return VisualizerUtil.thumbnailEnabled(task); } } /** * Recompute the view box of the plot. */ private void recalcViewbox() { // 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(); } /** * 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); } } /** * Cancel the overview, i.e. stop the thumbnailer */ @Override public void dispose() { context.removeResultListener(this); super.dispose(); } /** * @return the ratio */ public double getRatio() { return ratio; } /** * @param ratio the ratio to set */ public void setRatio(double ratio) { this.ratio = ratio; } /** * Trigger a redraw, but avoid excessive redraws. */ public final void lazyRefresh() { logger.debug("Scheduling refresh."); Runnable pr = new Runnable() { @Override public void run() { if(OverviewPlot.this.pendingRefresh == this) { OverviewPlot.this.refresh(); } } }; pendingRefresh = pr; scheduleUpdate(pr); } @Override public void resultAdded(Result child, Result parent) { logger.debug("result added: " + child); if(child instanceof VisualizationTask) { reinitOnRefresh = true; } lazyRefresh(); } @Override public void resultChanged(Result current) { logger.debug("result changed: " + current); lazyRefresh(); } @Override public void resultRemoved(Result child, Result parent) { logger.debug("result removed: " + child); lazyRefresh(); } }