/* * org.openmicroscopy.shoola.env.rnd.NavigationHistory * *------------------------------------------------------------------------------ * 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.util.ArrayList; import java.util.List; //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; /** * Keeps track of the XY planes, within a given pixels set, that have been * rendered and tries to guess the next ones that will be requested. * <p>We keep an history of {@link PlaneDef}s, one for each XY plane that has * been renderered. Obviously only the z and t coordinates of each entry are * relevant and it is trivial that an instance of {@link PlaneDef} can be * identified with a point in the <i>zOt</i> cartesian plane. Once a * {@link PlaneDef} has been added to the history, we refer to it as a * <i>move</i>. * History entries are kept in the same order as they were * {@link #addMove(PlaneDef) added}. The oldest entry is removed to make room * for a new one when {@link #MAX_ENTRIES} is reached.</p> * <p>If the history is not empty, then we call the most recent move the * <i>current move</i>. If the history has more than one entry, then we refer * to the second most recent move as to the <i>last move</i>. The last move * <i>L</i> and the current move <i>C</i> define a direction (the vector * <i>LC</i>), which we call the current (navigation) direction. Note that as * long as the history size is greater than two, the current direction is always * a line. * This is because a {@link PlaneDef} is never {@link #addMove(PlaneDef) added} * to the history if it is the same as the current move — that is, if we * haven't moved at all from the previous point, so the current and last moves * are always different.</p> * <p>Future moves are predicted in a trivial way, based on the two most recent * moves in the history — that is on the current navigation direction. * This class should eventually evolve to do something more sophisticated and * possibly apply different stratgies to predict upcoming moves based on the * analysis of the history.</p> * * @author Jean-Marie Burel      * <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a> * @author <br>Andrea Falconi      * <a href="mailto:a.falconi@dundee.ac.uk"> * a.falconi@dundee.ac.uk</a> * @version 2.2 * <small> * (<b>Internal version:</b> $Revision$ $Date$) * </small> * @since OME2.2 */ class NavigationHistory { /** The number of stack frames in the pixels set. */ final int SIZE_Z; /** The number of timepoints in the pixels set. */ final int SIZE_T; /** The maximum number of entries in the {@link #history}. */ final int MAX_ENTRIES; /** * List of {@link PlaneDef}s, one for each move. * That is, one for each XY plane that has been renderered. Obviously only * the z and t coordinates of each entry are relevant. Entries are kept in * the same order as they were {@link #addMove(PlaneDef) added}. The * oldest entry is {@link #ensureCapacity() removed} to make room for a new * one when {@link #MAX_ENTRIES} is reached. */ private List history; /** * Makes sure there's enough room in {@link #history} for a move to be * added. * If {@link #MAX_ENTRIES} has been reached, then we remove the oldest * entry in {@link #history}. That is, its first element. */ private void ensureCapacity() { if (history.size() >= MAX_ENTRIES) //2 at least, so there's element 0. history.remove(0); } /** * Returns the plane definition that was added by the second last call * to {@link #addMove(PlaneDef)}. * If the {@link #history} size is less than two, then <code>null</code> * is returned instead. * * @return See above. */ private PlaneDef lastMove() { int hSize = history.size(); if (1 < hSize) return (PlaneDef) history.get(hSize-2); return null; } /** * Returns the plane definition that was added by the last call * to {@link #addMove(PlaneDef)}. * If the {@link #history} size is less than one, then <code>null</code> * is returned instead. * * @return See above. */ private PlaneDef curMove() { int hSize = history.size(); if (0 < hSize) return (PlaneDef) history.get(hSize-1); return null; } /** * Creates a new instance. * * @param maxEntries Maximum number of entries to keep in the history. * If less than two, it will be set to two. * @param sizeZ The number of stack frames in the pixels set. * Must be positive. * @param sizeT The number of timepoints in the pixels set. * Must be positive. */ NavigationHistory(int maxEntries, int sizeZ, int sizeT) { if (sizeZ <= 0) throw new IllegalArgumentException( "Non-positive sizeZ: "+sizeZ+"."); if (sizeT <= 0) throw new IllegalArgumentException( "Non-positive sizeT: "+sizeT+"."); SIZE_Z = sizeZ; SIZE_T = sizeT; MAX_ENTRIES = (maxEntries < 2 ? 2 : maxEntries); history = new ArrayList(MAX_ENTRIES); } /** * Adds a move to the history. * This method is meant to record a move, so a copy of the passed argument * is made and then add it to the history. This way we avoid possible * inconsistencies if the caller modifies <code>pd</code> after calling * this method. * Note that this method won't add <code>pd</code> to the history if it is * the same as the current move — that is, if we haven't moved at all * from the previous point. * * @param pd Represents the move. Mustn't be <code>null</code>. * Moreover, only XY planes are accepted and their z, t * indexes must be within the bounds declared to the * constructor of this class: {@link #SIZE_Z}, {@link #SIZE_T}. */ void addMove(PlaneDef pd) { //First check pd is a good one. if (pd == null) throw new NullPointerException("No plane def."); int slice = pd.slice, z = pd.z, t = pd.t; if (slice != omero.romio.XY.value) throw new IllegalArgumentException( "Can only accept XY planes: "+slice+"."); if (SIZE_Z <= z) //PlaneDef already checks 0 <= z. throw new IllegalArgumentException("z not in [0, SIZE_Z="+SIZE_Z+ "): "+z+"."); if (SIZE_T <= t) //PlaneDef already checks 0 <= t. throw new IllegalArgumentException("t not in [0, SIZE_T="+SIZE_T+ "): "+t+"."); //Check if pd is the current move. If so, return as we haven't moved //at all from the previous point. if (pd.equals(curMove())) return; //curMove can be null, but pd is not. //Now make a copy to avoid caller changing entry after we added. pd = new PlaneDef(); pd.slice = omero.romio.XY.value; pd.t = t; pd.z = z; //Make room for pd if we reached MAX_ENTRIES and finally add. ensureCapacity(); history.add(pd); } /** * Returns the navigation direction with respect to the two most recent * moves. * If the history size is less than two, this method returns * <code>null</code> as the direction is undefined. Otherwise, said * <i>L</i> the last move and <i>C</i> the current move, this method * returns the line passing through <i>L</i> and <i>C</i>, having the same * orientation as the <i>LC</i> vector, and having its origin in <i>C</i>. * * @return One of the flags defined by this class. */ Line currentDirection() { int hSize = history.size(); //We need at least two moves to determine the direction. if (hSize < 2) return null; //Get the most two recent moves. Note they can't be null b/c addMove //doesn't allow null. Moreover, they represent different points b/c //addMove doesn't add the same move twice in a row. So we can build //a line. PlaneDef curMove = curMove(), lastMove = lastMove(); PlanePoint L = new PlanePoint(lastMove.z, lastMove.t), C = new PlanePoint(curMove.z, curMove.t); return new Line(L, C, C); //Direction LC and origin C. } /** * Predicts at most <code>maxMoves</code> upcoming moves. * Each move is represented by an instance of {@link PlaneDef} and all * of them are packed into the array returned by this method. This array * never contains the current move (the move added by the most recent call * to the {@link #addMove(PlaneDef) addMove} method) and can have less than * <code>maxMoves</code> elements if it wasn't possible to predict that much * moves. In particular, the length may be <code>0</code> if no prediction * could be made, but <code>null</code> is never returned. If the array * does contain elements, the first element (element at <code>0</code>) is * the first upcoming move, the second element is the second upcoming move, * and so on. * * @param maxMoves Maximum number of moves to predict. * @return An array containing the predicted moves. */ PlaneDef[] guessNextMoves(int maxMoves) { Line dir = currentDirection(); //We can't guess if we don't have at least two moves. Also return //if maxMoves <= 0. if (dir == null || maxMoves <= 0) return new PlaneDef[0]; //Never return null. List nextMoves = new ArrayList(maxMoves); PlanePoint p; PlaneDef pd; for (int k = 1; k <= maxMoves; ++k) { //Iterate maxMoves at most. //Get next point in the current direction. p = dir.getPoint(k); //See note. if (p.x1 < 0 || SIZE_Z <= p.x1 || p.x2 < 0 || SIZE_T <= p.x2) break; pd = new PlaneDef(); pd.slice = omero.romio.XY.value; pd.t = (int) p.x2; pd.y = (int) p.x1; //Even though dir.getPoint is monotonic, we could be getting a pd //equal to the previous one b/c of the above casts to int. However, //this shouldn't happen if navigation is || to the z or t axis. if (!nextMoves.contains(pd)) //Never allow duplicates. nextMoves.add(pd); } return (PlaneDef[]) nextMoves.toArray(new PlaneDef[0]); } /* NOTE: If C is the current move and L the last move, then dir is the * line C + ku, u being the unit vector built from LC. Thus the getPoint * method returns C + u, C + 2u, .. , C + mu (m being maxMoves). * For this reason, if navigation is parallel to the z or t axis, then * we move to the next integer coordinates along the line in the LC * direction -- casts to int shouldn't change anything in this case. * However, this class will keep on working even with fancy viewers (if * you can think of any) that don't navigate in directions parallel to the * z or t axis. The only thing to be careful about in that case is the * slope of the line. If too steep, then even large values of maxMoves * could result in no prediction -- b/c of the cast to int we could be * getting the same pd all the time. */ /* * ============================================================== * Follows code to enable testing. * ============================================================== */ /** * Returns the history. * * @return See above. */ List getHistory() { return history; } }