/* * org.openmicroscopy.shoola.env.rnd.XYCache * *------------------------------------------------------------------------------ * Copyright (C) 2006 University of Dundee. All rights reserved. * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * *------------------------------------------------------------------------------ */ package org.openmicroscopy.shoola.env.rnd; //Java imports import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; //Third-party libraries //Application-internal dependencies import omero.romio.PlaneDef; import org.openmicroscopy.shoola.util.math.geom2D.Line; import org.openmicroscopy.shoola.util.math.geom2D.PlanePoint; /** * Caches XY images, within a given pixels set, that have been rendered. * <p>The number of entries in the cache at any given time is * <code>max_entries</code> at most, being <code>max_entries</code> the greatest * integer such that <nobr> * <code>max_entries</code>*<code>image_size</code> <= * <code>cache_size</code> * </nobr>.</p> * <p>If <code>max_entries</code> is reached and an entry has to be added, we * discard a previous entry to make room for the new one. The removal policy * is based on the current navigation direction maintained by the * {@link NavigationHistory} and is as follows. Let <code>C</code> be the set * of all entries in the cache and be <code>n</code> its cardinality. It's a * trivial observation that we can identify an element of <code>C</code> with * a point in the <i>zOt</i> cartesian plane. Now if a point <code>p</code> is * to be added to <code>C</code> and <code>n=max_entries</code>, we consider * the set <code>C'</code> of all elements of <code>C</code> ordered such that * the first element is the farthest away from <code>p</code> and the last * element is the closest to <code>p</code>. That is:</p> * <p><code> * C' = {c<sub>1</sub>, .. , c<sub>n</sub>} <br> * d(c<sub>i</sub>, p) >= d(c<sub>i+i</sub>, p) </code><br> * <small>(d being the standard distance between two points in the cartesian * plane)</small> * </p> * <p>Then the removal algorithm is given by the following steps:</p> * <ol> * <li>Get the line <code>D</code> representing the current navigation * direction.</li> * <li>Look for the first element of <code>C'</code> that doesn't lie on * <code>D</code>. If such an element exists, then remove it. Otherwise * go on to the next step.</li> * <li>Look for the first element of <code>C'</code> that falls on the * negative half of <code>D</code> — this half contains the points * "behind" the current move with respect to the orientation of the * movement. If such an element exists, then remove it. Otherwise go on * to the next step.</li> * <li>Remove <code>c<sub>1</sub></code>.</li> * </ol> * * @author Jean-Marie Burel      * <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a> * @author Andrea Falconi      * <a href="mailto:a.falconi@dundee.ac.uk">a.falconi@dundee.ac.uk</a> * @version 3.0 * <small> * (<b>Internal version:</b> $Revision: $ $Date: $) * </small> * @since OME2.2 */ public class XYCache { /** The size, in bytes, of a rendered XY image. */ private final int image_size; /** * The size, in bytes, of the image cache. * Rendered images will be cached until we reach this value. Subsequent * requests to cache an image will result in the removal of some already * cached images. */ private int cache_size; /** * Maximum number of entries allowed in the cache. * This is the greatest integer <code>M</code> such that * <nobr><code> * M*{@link #image_size} <= {@link #cache_size}</code></nobr>. */ private int max_entries; /** * Maps {@link PlanePoint}s onto {@link BufferedImage}s or * <code>byte</code> array. * The <code>add</code> method identifies a {@link PlaneDef} object with * a point in the <i>zOt</i> cartesian plane, hence with an instance of * {@link PlanePoint}. */ private Map<PlanePoint, Object> cache; /** * Refers to the {@link NavigationHistory} serving the pixels set this * cache was associated to. */ private NavigationHistory navigHistory; /** * Makes enough room in {@link #cache} for a new entry to be added. * We remove an existing entry according to the removal algorithm * specified by this class. It is assumed that this method will * only be invoked when the cache size equals {@link #max_entries} * and is one at least. * * @param p The key for the new entry that has to be added. * It's assumed the caller will never pass <code>null</code>. */ private void ensureCapacity(final PlanePoint p) { //First off, build the C' sequence. PlanePoint[] orderedCache = cache.keySet().toArray(new PlanePoint[0]); Arrays.sort(orderedCache, new Comparator() { public int compare(Object o1, Object o2) { PlanePoint c1 = (PlanePoint) o1, c2 = (PlanePoint) o2; return -Double.compare(c1.distance(p), c2.distance(p)); //Note the minus above: we want descending order. } }); //Now get the current navigation direction and set the default //candidate for removal: the farthest point away from p. Line curDir = navigHistory.currentDirection(); PlanePoint candidate = orderedCache[0]; //We assume cache size > 0. //Start the removal algorithm if the navigation direction is defined. if (curDir != null) { List negativeHalf = new ArrayList(orderedCache.length); int i = 0; for (; i < orderedCache.length; ++i) { //Does candidate lie on curDir at all? if (!curDir.lies(orderedCache[i])) { //No. This is the farthest point away from p not on curDir. candidate = orderedCache[i]; break; } //Then orderedCache[i] lies on curDir, which half though? if (curDir.lies(orderedCache[i], false)) //It lies on the negative half. This means it sits behind //the current move w/r/t movement orientation. Collect it. //Notice that points in this list are such that: // d(negativeHalf(i), p) >= d(negativeHalf(i+1), p) negativeHalf.add(orderedCache[i]); } if (i == orderedCache.length //All cached points lie on curDir. && !negativeHalf.isEmpty()) //But some in neg half. //Get farthest point away from p that sits behind current move. candidate = (PlanePoint) negativeHalf.get(0); } //Finally remove. cache.remove(candidate); } /** * Creates a new instance. * An <code>ImageCache</code> works with a given pixels set and * with the {@link NavigationHistory} serving that pixels set. The * <code>cacheSize</code> and <code>imageSize</code> parameters determine * how many {@link #max_entries entries} the cache will allow before * purging old entries. In particular, if the <code>imageSize</code> is * greater than the <code>cacheSize</code>, no element will ever be * cached. * * @param cacheSize The size, in bytes, of the cache. Must be positive. * @param imageSize The size, in bytes, of an XY image. Must be positive. * @param nh Reference to the {@link NavigationHistory} serving * the pixels set this cache was associated to. * Mustn't be <code>null</code>. */ XYCache(int cacheSize, int imageSize, NavigationHistory nh) { if (cacheSize < 0) throw new IllegalArgumentException( "Cache size must be positive: "+cacheSize+"."); if (imageSize <= 0) throw new IllegalArgumentException( "Image size must be positive: "+imageSize+"."); if (nh == null) throw new NullPointerException("No navigation history."); cache_size = cacheSize; image_size = imageSize; max_entries = cache_size/image_size; cache = new HashMap<PlanePoint, Object>(max_entries); navigHistory = nh; } /** * Adds the specified entry to the cache. * * @param pd The key. Mustn't be <code>null</code> and must define * an XY plane. * @param object An XY image or a byte array. * Mustn't be <code>null</code>. */ void add(PlaneDef pd, Object object) { if (max_entries == 0) return; //Caching disabled. //Sanity checks. if (pd == null) throw new NullPointerException("No plane def."); if (pd.slice != omero.romio.XY.value) throw new IllegalArgumentException( "Can only accept XY planes: "+pd.slice+"."); if (object == null) throw new NullPointerException("No image."); //Will the next entry fit into the cache? PlanePoint key = new PlanePoint(pd.z, pd.t); if (max_entries <= cache.size()) //Nope, make room for it. ensureCapacity(key); //Once we're here we have enough room for the new element. cache.put(key, object); } /** * Extracts the image (if any) associated to <code>pd</code>. * * @param pd The key. Mustn't be <code>null</code> and must define * an XY plane. * @return The image or byte array associated to <code>pd</code> or * <code>null</code> if the cache doesn't contain such an * entry. */ Object extract(PlaneDef pd) { if (pd == null) throw new NullPointerException("No plane def."); if (pd.slice != omero.romio.XY.value) throw new IllegalArgumentException( "Can only accept XY planes: "+pd.slice+"."); PlanePoint key = new PlanePoint(pd.z, pd.t); return cache.get(key); } /** * Tells whether or not the cache contains an entry for the specified * plane definition. * * @param pd The key. * @return <code>true</code> if the cache contains an entry for * <code>pd</code>, <code>false</code> otherwise. * @see #add(PlaneDef, BufferedImage) */ boolean contains(PlaneDef pd) { if (pd == null) return false; PlanePoint key = new PlanePoint(pd.z, pd.t); return (cache.get(key) != null); } /** Removes all the entries from the cache. */ void clear() { int oldSize = cache.size(); cache = new HashMap<PlanePoint, Object>(oldSize); } /** * Resets the size of the cache. * * @param size */ void resetCacheSize(int size) { if (size < 0) throw new IllegalArgumentException( "Cache size must be positive: "+size+"."); cache_size = size; max_entries = cache_size/image_size; cache = new HashMap<PlanePoint, Object>(max_entries); } /* * ============================================================== * Follows code to enable testing. * ============================================================== */ /** * Returns the cache. * * @return See above. */ Map getCache() { return cache; } /** * Returns the navigation history. * * @return See above. */ NavigationHistory getNavigHistory() { return navigHistory; } }