/* * Copyright 2004-2010 Information & Software Engineering Group (188/1) * Institute of Software Technology and Interactive Systems * Vienna University of Technology, Austria * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.ifs.tuwien.ac.at/dm/somtoolbox/license.html * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package at.tuwien.ifs.somtoolbox.visualization; import java.awt.AlphaComposite; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Vector; import java.util.logging.Logger; import javax.swing.JCheckBox; import javax.swing.JComboBox; 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; import org.apache.commons.lang.ArrayUtils; import at.tuwien.ifs.somtoolbox.SOMToolboxException; import at.tuwien.ifs.somtoolbox.apps.viewer.MapPNode; import at.tuwien.ifs.somtoolbox.data.SharedSOMVisualisationData; import at.tuwien.ifs.somtoolbox.input.SOMInputReader; import at.tuwien.ifs.somtoolbox.models.GrowingSOM; import at.tuwien.ifs.somtoolbox.util.LeastRecentelyUsedImageCache; import at.tuwien.ifs.somtoolbox.util.StringUtils; /** * This class provides basic support for classes implementing {@link BackgroundImageVisualizer}. * * @author Michael Dittenbach * @author Rudolf Mayer * @version $Id: AbstractBackgroundImageVisualizer.java 3888 2010-11-02 17:42:53Z frank $ */ public abstract class AbstractBackgroundImageVisualizer implements BackgroundImageVisualizer { protected enum ContourMode { None, Overlay, Full }; protected enum ContourInterpolationMode { Linear, Log }; public static final String CACHE_KEY_SEPARATOR = ", "; // 250 MB cache for visualisation images, max half memory size public static final long MAX_CACHE_SIZE_MB = Math.min(250 * 1024 * 1024, Runtime.getRuntime().maxMemory() / 2); protected static final String CACHE_KEY_SECTION_SEPARATOR = " >> "; /** * The number of visualisation variants this visualizer provides */ protected int NUM_VISUALIZATIONS = 0; /** * The names of the visualisation variants. */ protected String[] VISUALIZATION_NAMES = null; protected String[] VISUALIZATION_SHORT_NAMES = null; /** * Longer description for the visualiation variants. Can e.g. be references to the algorithm, etc. */ protected String[] VISUALIZATION_DESCRIPTIONS = null; /** * The panel to control the behaviour of the visualisation. */ protected VisualizationControlPanel controlPanel; /** * The cache of generated images, to allow faster switching between different visualisations and palettes. Note that * the cache is static, i.e. only one cache for all visualisation subclasses is used. */ protected static final LeastRecentelyUsedImageCache cache = new LeastRecentelyUsedImageCache(MAX_CACHE_SIZE_MB); /** The standard log for visualisations to write to */ protected Logger log = Logger.getLogger("at.tuwien.ifs.somtoolbox"); /** * The listener registered to act on changing visualisation variants or other properties. */ protected VisualizationUpdateListener visualizationUpdateListener = null; protected SharedSOMVisualisationData inputObjects; protected String[] neededInputObjects; protected MapPNode map; /** * The opacity (transparency) value for this visualisation. 100 means no transparency, while 0 means total * transparency. */ protected int opacity = 100; protected boolean interpolate = true; protected ContourMode contourMode = ContourMode.None; protected int numberOfContours = 7; protected ContourInterpolationMode contourInterpolationMode = ContourInterpolationMode.Linear; // FIXME: the selection of the x-dim should be moved to the mapPane, as it doesn't only affect the visualisation, // but the whole viewer appearance, // i.e. also the labels displayed, the pie-charts, etc.. public int zSize = 1; public int currentZDimSlice = 0; /** * Initialised with the default value from {@link BackgroundImageVisualizer#DEFAULT_BACKGROUND_VISUALIZATION_SCALE}. * Visualisations that need a specific scale shall set this value differently (e.g. in the constructor), or * overwrite {@link #getPreferredScaleFactor()} */ protected int preferredScaleFactor = DEFAULT_BACKGROUND_VISUALIZATION_SCALE; /** * Initialises the control panel, if {@link GraphicsEnvironment#isHeadless()} reports to be in a non-headless * environment. */ public AbstractBackgroundImageVisualizer() { super(); // don't initialise the control panel if we have no graphics environment (e.g. in server applications) if (!GraphicsEnvironment.isHeadless()) { try { controlPanel = new VisualizationControlPanel("Visualisation Control"); } catch (Throwable e) { Logger.getLogger("at.tuwien.ifs.somtoolbox").severe( "Caught runtime exception/error during graphics init: " + e.getMessage() + "\n Headless environment? " + GraphicsEnvironment.isHeadless()); } } } /** * The key of a cache is created as follows: VisualisationShortName + Hashcode of the SOM + Width + Height + * Opacity.<br/> * Sub-classes might add more information to the cache, if needed. */ protected String getCacheKey(GrowingSOM gsom, int currentVariant, int width, int height) { return getBasicCacheKey(gsom, currentVariant, width, height); } private String getBasicCacheKey(GrowingSOM gsom, int currentVariant, int width, int height) { return buildCacheKey(getVisualizationShortName(currentVariant), "SOM:" + gsom.hashCode(), width + "x" + height, "opac:" + opacity); } /** * Returns the requested visualization image, either by retrieving it from the image cache, or by invoking * {@link #createVisualization(int, GrowingSOM, int, int)} to create the image new. Subclasses should not overwrite * this method, unless they implement their own caching mechanism, or do not want any caching. * * @see at.tuwien.ifs.somtoolbox.visualization.BackgroundImageVisualizer#getVisualization(int, * at.tuwien.ifs.somtoolbox.models.GrowingSOM, int, int) */ @Override public BufferedImage getVisualization(int index, GrowingSOM gsom, int width, int height) throws SOMToolboxException { controlPanel.updateZDim(gsom.getLayer().getZSize()); String cacheKey = getCacheKey(gsom, index, width, height); logImageCache(cacheKey); if (cache.get(cacheKey) == null) { cache.put(cacheKey, createVisualization(index, gsom, width, height)); } return cache.get(cacheKey); } protected void logImageCache(String cacheKey) { Logger.getLogger("at.tuwien.ifs.somtoolbox").info( (cache.get(cacheKey) == null ? "Creating" : "Loading ") + " image, cache size: " + cache.size() + ", key: " + cacheKey); } public String appendToCacheKey(GrowingSOM gsom, int currentVariant, int width, int height, Object... parts) { return getBasicCacheKey(gsom, currentVariant, width, height) + CACHE_KEY_SECTION_SEPARATOR + buildCacheKey(parts); } public static String buildCacheKey(Object... parts) { StringBuilder sb = new StringBuilder(); for (Object part : parts) { if (sb.length() > 0) { sb.append(CACHE_KEY_SEPARATOR); } sb.append(part); } return sb.toString(); } /** Deletes all cached elements from the given visualisation. */ protected void invalidateCache(final String visualizationName) { for (String key : new ArrayList<String>(cache.keySet())) { // use a copy to avoid concurrency issues when // removing elements below if (key.startsWith(visualizationName)) { cache.remove(key); Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Removed cache for: " + key); } } } /** * Creates a visualisation image. Subclasses must implement this method. * * @param variantIndex the index of the variant to use * @param gsom the GrowingSOM to take build the visualisation for * @param width the desired width of the image, in pixels * @param height the desired height of the image, in pixels. * @return an image for this visualisation. */ public abstract BufferedImage createVisualization(int variantIndex, GrowingSOM gsom, int width, int height) throws SOMToolboxException; /** * Creates a visualisation for the given variant name. Basically just a call to * {@link #createVisualization(int, GrowingSOM, int, int)}, but throws a {@link SOMToolboxException} if the given * variant name is not known in either the {@link #VISUALIZATION_NAMES} nor {@link #VISUALIZATION_SHORT_NAMES}. */ public BufferedImage createVisualization(String variantName, GrowingSOM gsom, int width, int height) throws SOMToolboxException { int i = ArrayUtils.indexOf(VISUALIZATION_NAMES, variantName); if (i == ArrayUtils.INDEX_NOT_FOUND) { i = ArrayUtils.indexOf(VISUALIZATION_SHORT_NAMES, variantName); } if (i == ArrayUtils.INDEX_NOT_FOUND) { throw new SOMToolboxException("Unkown visualisation variant '" + variantName + "' for " + getClass().getName()); } else { return createVisualization(i, gsom, width, height); } } @Override public int getNumberOfVisualizations() { return NUM_VISUALIZATIONS; } protected void checkVariantIndex(int index, Class<?> klass) throws SOMToolboxException { if (index < 0 || index >= NUM_VISUALIZATIONS) { throw getVariantException(index, klass); } } protected SOMToolboxException getVariantException(int index, Class<?> klass) { return new SOMToolboxException("Illegal variant index " + index + " for " + klass.getSimpleName() + ", only " + NUM_VISUALIZATIONS + " are available."); } @Override public String getVisualizationDescription(int index) { if (NUM_VISUALIZATIONS > 0) { return VISUALIZATION_DESCRIPTIONS[index]; } else { return null; } } @Override public String[] getVisualizationDescriptions() { return VISUALIZATION_DESCRIPTIONS; } @Override public String getVisualizationName(int index) { if (NUM_VISUALIZATIONS > 0) { return VISUALIZATION_NAMES[index]; } else { return null; } } @Override public String[] getVisualizationNames() { return VISUALIZATION_NAMES; } @Override public String getVisualizationShortName(int index) { if (NUM_VISUALIZATIONS > 0) { return VISUALIZATION_SHORT_NAMES[index]; } else { return null; } } @Override public String[] getVisualizationShortNames() { return VISUALIZATION_SHORT_NAMES; } @Override public String[] needsAdditionalFiles() { if (neededInputObjects == null) { return null; } Vector<String> neededFiles = new Vector<String>(); for (int i = 0; i < neededInputObjects.length; i++) { if (!inputObjects.getObject(neededInputObjects[i]).hasData()) { neededFiles.add(neededInputObjects[i]); } } return neededFiles.toArray(new String[neededFiles.size()]); } protected void checkNeededObjectsAvailable(GrowingSOM gsom) throws SOMToolboxException { String[] needsAdditionalFiles = needsAdditionalFiles(); if (needsAdditionalFiles != null && needsAdditionalFiles.length > 0) { throw new SOMToolboxException(Arrays.toString(needsAdditionalFiles) + " required."); } } @Override public VisualizationControlPanel getControlPanel() { return controlPanel; } @Override public void setVisualizationUpdateListener(VisualizationUpdateListener listener) { visualizationUpdateListener = listener; } /** * Implementing sub-classes shall override this method if they need to set some specific input object related * information. */ @Override public void setInputObjects(SharedSOMVisualisationData inputObjects) { this.inputObjects = inputObjects; } /** * Implementing sub-classes shall override this method if they need to set some specific input-data related * information. */ @Override public void setSOMData(SOMInputReader reader) { } /** * A basic visualisation control panel, providing a {@linkplain JSpinner} to control the opacity. Visualisations * that require more control elements should extend this class, and add their own elements. * * @author Rudolf Mayer */ public class VisualizationControlPanel extends JPanel implements ComponentListener { private static final long serialVersionUID = 1L; protected final JSpinner opacitySpinner = new JSpinner(new SpinnerNumberModel(opacity, 0, 100, 1)); // values in // % opacity protected final JCheckBox interpolateCheckbox = new JCheckBox("Interpolate"); protected final JComboBox contourComboBox = new JComboBox(ContourMode.values()); public final Font smallerFont = new Font("Tahoma_small", Font.PLAIN, 9); public final Font reallySmallerFont = new Font("Tahoma_small", Font.PLAIN, 8); /** * The {@link GridBagConstraints} to be used to add components. */ protected GridBagConstraints c = new GridBagConstraints(); public JSpinner spinnerZSlice = null; /** Constructs a new VisualizationControlPanel with a specific name, using a {@link GridBagLayout},. */ public VisualizationControlPanel(String name) { super(new GridBagLayout()); setName(name); c.gridy = 0; c.gridx = GridBagConstraints.REMAINDER; c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 1.0; opacitySpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { opacity = ((Integer) ((JSpinner) e.getSource()).getValue()).intValue(); if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } }); JPanel basicPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); basicPanel.add(new JLabel("Opacity: ")); basicPanel.add(opacitySpinner); opacitySpinner.setFont(smallerFont); // interpolate spinner, only for matrix visualizers! if (AbstractBackgroundImageVisualizer.this instanceof MatrixVisualizer) { basicPanel.add(interpolateCheckbox); interpolateCheckbox.setSelected(interpolate); interpolateCheckbox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { interpolate = interpolateCheckbox.isSelected(); if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } }); } add(basicPanel, c); if (AbstractBackgroundImageVisualizer.this instanceof MatrixVisualizer) { JPanel contourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); contourPanel.add(new JLabel("Contour: ")); contourPanel.add(contourComboBox); contourComboBox.setToolTipText("Select the contour display mode (" + StringUtils.toString(ContourMode.values(), "", "") + ")"); contourComboBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { contourMode = (ContourMode) contourComboBox.getSelectedItem(); if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } }); final JComboBox contourInterpolationComboBox = new JComboBox(ContourInterpolationMode.values()); contourInterpolationComboBox.setToolTipText("Select the contour interpolation method (" + StringUtils.toString(ContourInterpolationMode.values(), "", "") + ")"); contourPanel.add(contourInterpolationComboBox); contourInterpolationComboBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { contourInterpolationMode = (ContourInterpolationMode) contourInterpolationComboBox.getSelectedItem(); if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } }); final JSpinner numberOfContoursSpinner = new JSpinner( new SpinnerNumberModel(numberOfContours, 2, 30, 1)); numberOfContoursSpinner.setToolTipText("Select the number of contour lines"); contourPanel.add(numberOfContoursSpinner); numberOfContoursSpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { numberOfContours = (Integer) numberOfContoursSpinner.getValue(); if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } }); c.gridy += 1; add(contourPanel, c); } // New, by frank // FIXME: the slice panel should not be in the visualisation, rather somewhere in the top-menu, as it should // also be possible to slice // through the SOM Cube when there is not visualisation active JPanel slicePanel = new JPanel(); slicePanel.add(new JLabel("Show slice")); spinnerZSlice = new JSpinner(); spinnerZSlice.setModel(new SpinnerNumberModel(currentZDimSlice, 0, zSize, 1)); spinnerZSlice.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { JSpinner src = (JSpinner) e.getSource(); currentZDimSlice = ((Integer) src.getValue()).intValue(); if (visualizationUpdateListener != null) { visualizationUpdateListener.updateVisualization(); } } }); slicePanel.add(spinnerZSlice); // Z-Slicer temporarily not active, see https://olymp.ifs.tuwien.ac.at/trac/somtoolbox/ticket/151 // c.gridy += 1; // add(slicePanel, c); c.gridy += 1; } public void updateSwitchControls() { if (canSwitch()) { enableSwitchControls(true); } else { enableSwitchControls(false); } } private void enableSwitchControls(boolean enabled) { opacitySpinner.setEnabled(enabled); } private boolean canSwitch() { if (map != null) { return map.getCurrentVisualization() != null && map.isBackgroundImageVisible(); } else { return false; } } @Override public void componentHidden(ComponentEvent e) { // no specific action needed in this class, sub-classes shall // override } @Override public void componentMoved(ComponentEvent e) { // no specific action needed in this class, sub-classes shall // override } @Override public void componentResized(ComponentEvent e) {// no specific action needed in this class, sub-classes shall // override } @Override public void componentShown(ComponentEvent e) { // no specific action needed in this class, sub-classes shall // override } protected void updateZDim(int zDim) { if (AbstractBackgroundImageVisualizer.this.zSize != zDim) { AbstractBackgroundImageVisualizer.this.zSize = zDim; spinnerZSlice.setModel(new SpinnerNumberModel(currentZDimSlice, 0, zDim, 1)); spinnerZSlice.setEnabled(zDim > 1); spinnerZSlice.setVisible(zDim > 1); // Force repaint revalidate(); repaint(); } } } @Override public void setMap(MapPNode map) { this.map = map; } /** * Draws a background image on the given graphics object, and sets the Composite according to the currentely set * opacity value. * * @param width the desired width of the background image, in pixels * @param height the desired height of the background image, in pixels * @param g the graphics to draw on. */ protected void drawBackground(int width, int height, Graphics2D g) { // if background image is available, draw it on this graphics object if (map != null && map.getBackgroundImage() != null) { g.drawImage(map.getBackgroundImage(), 0, 0, width, height, null); } // set opacity factor for alpha composition float alpha = opacity / 100f; g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); } @Override @SuppressWarnings("rawtypes") public String getHTMLVisualisationControl(Map params) { return ""; } /** * Default implementation returning {@link #preferredScaleFactor}. Visualisations that need a specific scale factor * shall set the value of this field differently (e.g. in their constructor), or overwrite this method. */ @Override public int getPreferredScaleFactor() { return preferredScaleFactor; } @Override // Sorting visualisations after two criterea: // - first, QualityMeasureVisualizer are always last // - secondly, by the name of the first visualisation public int compareTo(BackgroundImageVisualizer o) { if (o instanceof QualityMeasureVisualizer) { if (this instanceof QualityMeasureVisualizer) { return getVisualizationName(0).compareTo(o.getVisualizationName(0)); } else { return -1; } } else { if (this instanceof QualityMeasureVisualizer) { return 1; } else if (o instanceof ComparisonVisualizer) { if (this instanceof ComparisonVisualizer) { return getVisualizationName(0).compareTo(o.getVisualizationName(0)); } else { return -1; } } else if (this instanceof ComparisonVisualizer) { return 1; } return getVisualizationName(0).compareTo(o.getVisualizationName(0)); } } /** * Default implementation which returns a map of size 1 with the standard, unparameterised visualisation of the * given variant. Subclasses that want to return more flavours should override this method. */ @Override public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height) throws SOMToolboxException { HashMap<String, BufferedImage> hashMap = new HashMap<String, BufferedImage>(1, 1); // size 1, load factor 1 hashMap.put("", getVisualization(index, gsom, width, height)); return hashMap; } /** Default implementation equal to {@link #getVisualizationFlavours(int, GrowingSOM, int, int)}. */ @Override public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height, int maxFlavours) throws SOMToolboxException { return getVisualizationFlavours(index, gsom, width, height); } /** Default implementation equal to {@link #getVisualizationFlavours(int, GrowingSOM, int, int)}. */ @Override public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height, Map<String, String> flavourParameters) throws SOMToolboxException { return getVisualizationFlavours(index, gsom, width, height); } /** Clears the visualisation cache */ public static void clearVisualisationCache() { cache.clear(); Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Cleared visualisation cache"); } }