/** TrakEM2 plugin for ImageJ(C). Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas. 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 (http://www.gnu.org/licenses/gpl.txt ) 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. You may contact Albert Cardona at acardona at ini.phys.ethz.ch Institute of Neuroinformatics, University of Zurich / ETH, Switzerland. **/ package ini.trakem2.display; import ij.ImagePlus; import ij.ImageStack; import ij.gui.GenericDialog; import ij.measure.Calibration; import ini.trakem2.ControlWindow; import ini.trakem2.Project; import ini.trakem2.imaging.LayerStack; import ini.trakem2.parallel.Process; import ini.trakem2.parallel.TaskFactory; import ini.trakem2.persistence.DBObject; import ini.trakem2.persistence.XMLOptions; import ini.trakem2.tree.LayerThing; import ini.trakem2.tree.ProjectThing; import ini.trakem2.tree.TemplateThing; import ini.trakem2.tree.Thing; import ini.trakem2.utils.IJError; import ini.trakem2.utils.ProjectToolbar; import ini.trakem2.utils.Utils; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.helpers.DefaultHandler; /** A LayerSet is a container for a list of Layer. * LayerSet methods are NOT synchronized. It is your reponsibility to synchronize access to a LayerSet instance methods. Failure to do so may result in corrupted internal data structures and overall misbehavior. */ public final class LayerSet extends Displayable implements Bucketable { // Displayable is already extending DBObject // the anchors for resizing static public final int NORTH = 0; static public final int NORTHEAST = 1; static public final int EAST = 2; static public final int SOUTHEAST = 3; static public final int SOUTH = 4; static public final int SOUTHWEST = 5; static public final int WEST = 6; static public final int NORTHWEST = 7; static public final int CENTER = 8; // the possible rotations static public final int R90 = 9; static public final int R270 = 10; // the possible flips static public final int FLIP_HORIZONTAL = 11; static public final int FLIP_VERTICAL = 12; // positions in the stack static public final int TOP = 13; static public final int UP = 14; static public final int DOWN = 15; static public final int BOTTOM = 16; static public final String[] snapshot_modes = new String[]{"Full","Outlines","Disabled"}; /** 0, 1, 2 -- corresponding to snapshot_modes entries above. */ private int snapshots_mode = 0; static public final String[] ANCHORS = new String[]{"north", "north east", "east", "southeast", "south", "south west", "west", "north west", "center"}; static public final String[] ROTATIONS = new String[]{"90 right", "90 left", "Flip horizontally", "Flip vertically"}; private float layer_width = 5000, // the Displayable.width is for the representation, not for the dimensions of the LayerSet! layer_height = 5000; private double rot_x; private double rot_y; private double rot_z; // should be equivalent to the Displayable.rot private final ArrayList<Layer> al_layers = new ArrayList<Layer>(); /** A map of Long vs Layer, that is lock-free for reading, but locks for modifying it, * by synchronizing onto IDLAYERS_WRITE_LOCK. */ private HashMap<Long,Layer> idlayers = new HashMap<Long,Layer>(); private final Object IDLAYERS_WRITE_LOCK = new Object(); private final HashMap<Layer,Integer> layerindices = new HashMap<Layer,Integer>(); /** The layer in which this LayerSet lives. If null, this is the root LayerSet. */ private Layer parent = null; /** A LayerSet can contain Displayables that are show in every single Layer, such as Pipe objects. */ private final ArrayList<ZDisplayable> al_zdispl = new ArrayList<ZDisplayable>(); /** For creating snapshots. */ private boolean snapshots_quality = true; /** The maximum size of either width or height when virtualizing pixel access to the layers.*/ private int max_dimension = 1024; private boolean virtualization_enabled = false; protected boolean color_cues = true; protected boolean area_color_cues = true; protected boolean use_color_cue_colors = true; protected boolean paint_arrows = true; protected boolean paint_tags = true; protected boolean paint_edge_confidence_boxes = true; protected int n_layers_color_cue = 0; // -1 means all protected boolean prepaint = false; protected int preload_ahead = 0; private Calibration calibration = new Calibration(); // default values /** Dummy. */ protected LayerSet(Project project, long id) { super(project, id, null, false, null, 20, 20); } /** Create a new LayerSet with a 0,0,0 rotation vector and default 20,20 px Displayable width,height. */ public LayerSet(Project project, String title, double x, double y, Layer parent, float layer_width, float layer_height) { super(project, title, x, y); rot_x = rot_y = rot_z = 0.0D; this.width = 20; this.height = 20; // for the label that paints into the parent Layer this.parent = parent; this.layer_width = layer_width; this.layer_height = layer_height; addToDatabase(); } /** Reconstruct from the database. */ public LayerSet(Project project, long id, String title, float width, float height, double rot_x, double rot_y, double rot_z, float layer_width, float layer_height, boolean locked, int snapshots_mode, AffineTransform at) { super(project, id, title, locked, at, width, height); this.rot_x = rot_x; this.rot_y = rot_y; this.rot_z = rot_z; this.layer_width = layer_width; this.layer_height= layer_height; this.snapshots_mode = snapshots_mode; // the parent will be set by the LayerThing.setup() calling Layer.addSilently() // the al_layers will be filled idem. } /** Reconstruct from an XML entry. */ public LayerSet(final Project project, final long id, final HashMap<String,String> ht_attributes, final HashMap<Displayable,String> ht_links) { super(project, id, ht_attributes, ht_links); String data; if (null != (data = ht_attributes.get("layer_width"))) this.layer_width = Float.parseFloat(data); else xmlError("layer_width", this.layer_width); if (null != (data = ht_attributes.get("layer_height"))) this.layer_height = Float.parseFloat(data); else xmlError("layer_height", this.layer_height); if (null != (data = ht_attributes.get("rot_x"))) this.rot_x = Double.parseDouble(data); else xmlError("rot_x", this.rot_x); if (null != (data = ht_attributes.get("rot_y"))) this.rot_y = Double.parseDouble(data); else xmlError("rot_y", this.rot_y); if (null != (data = ht_attributes.get("rot_z"))) this.rot_y = Double.parseDouble(data); else xmlError("rot_z", this.rot_z); if (null != (data = ht_attributes.get("snapshots_quality"))) snapshots_quality = Boolean.valueOf(data.trim().toLowerCase()); if (null != (data = ht_attributes.get("snapshots_mode"))) { final String smode = data.trim(); for (int i=0; i<snapshot_modes.length; i++) { if (smode.equals(snapshot_modes[i])) { snapshots_mode = i; break; } } } if (null != (data = ht_attributes.get("color_cues"))) color_cues = Boolean.valueOf(data.trim().toLowerCase()); if (null != (data = ht_attributes.get("area_color_cues"))) area_color_cues = Boolean.valueOf(data.trim().toLowerCase()); if (null != (data = ht_attributes.get("n_layers_color_cue"))) { n_layers_color_cue = Integer.parseInt(data.trim().toLowerCase()); if (n_layers_color_cue < -1) n_layers_color_cue = -1; } if (null != (data = ht_attributes.get("avoid_color_cue_colors"))) { // If there's any error in the parsing, default to true for use_color_cue_colors: use_color_cue_colors = !Boolean.valueOf(data.trim().toLowerCase()); } if (null != (data = ht_attributes.get("paint_arrows"))) paint_arrows = Boolean.valueOf(data.trim().toLowerCase()); if (null != (data = ht_attributes.get("paint_tags"))) paint_tags = Boolean.valueOf(data.trim().toLowerCase()); if (null != (data = ht_attributes.get("paint_edge_confidence_boxes"))) paint_edge_confidence_boxes = Boolean.valueOf(data.trim().toLowerCase()); if (null != (data = ht_attributes.get("prepaint"))) prepaint = Boolean.valueOf(data.trim().toLowerCase()); if (null != (data = ht_attributes.get("preload_ahead"))) preload_ahead = Integer.parseInt(data); } /** For reconstruction purposes: set the active layer to the ZDisplayable objects. Recurses through LayerSets in the children layers. */ public void setup() { final Layer la0 = al_layers.get(0); for (ZDisplayable zd : al_zdispl) zd.setLayer(la0); // just any Layer for (Layer layer : al_layers) { for (final Displayable d : layer.getDisplayables()) { if (d.getClass() == LayerSet.class) { ((LayerSet)d).setup(); } } } } /** Create a new LayerSet in the middle of the parent Layer. */ public LayerSet create(Layer parent_layer) { if (null == parent_layer) return null; GenericDialog gd = ControlWindow.makeGenericDialog("New Layer Set"); gd.addMessage("In pixels:"); gd.addNumericField("width: ", this.layer_width, 3); gd.addNumericField("height: ", this.layer_height, 3); gd.showDialog(); if (gd.wasCanceled()) return null; try { float width = (float)gd.getNextNumber(); float height = (float)gd.getNextNumber(); if (Double.isNaN(width) || Double.isNaN(height)) return null; if (0 == width || 0 == height) { Utils.showMessage("Cannot accept zero width or height for LayerSet dimensions."); return null; } // make a new LayerSet with x,y in the middle of the parent_layer return new LayerSet(project, "Layer Set", parent_layer.getParent().getLayerWidth() / 2, parent_layer.getParent().getLayerHeight() / 2, parent_layer, width/2, height/2); } catch (Exception e) { Utils.log("LayerSet.create: " + e); } return null; } /** Add a new Layer silently, ordering by z as well.*/ public void addSilently(final Layer layer) { if (null == layer || al_layers.contains(layer)) return; try { synchronized (IDLAYERS_WRITE_LOCK) { // Like put, but replacing the map instance final HashMap<Long,Layer> m = new HashMap<Long,Layer>(idlayers); m.put(layer.getId(), layer); idlayers = m; } synchronized (layerindices) { layerindices.clear(); } double z = layer.getZ(); int i = 0; for (final Layer la : al_layers) { if (! (la.getZ() < z) ) { al_layers.add(i, layer); layer.setParentSilently(this); return; } i++; } // else, add at the end al_layers.add(layer); layer.setParentSilently(this); } catch (Exception e) { Utils.log("LayerSet.addSilently: Not a Layer, not adding DBObject id=" + layer.getId()); return; } } /** Add a new Layer, inserted according to its Z. */ public void add(final Layer layer) { if (layer.getProject() != this.project) throw new IllegalArgumentException("LayerSet rejected a Layer: belongs to a different project."); if (null != idlayers.get(layer.getId())) return; final double z = layer.getZ(); final int n = al_layers.size(); int i = 0; for (; i<n; i++) { Layer l = al_layers.get(i); if (l.getZ() < z) continue; break; } if (i < n) { al_layers.add(i, layer); } else { al_layers.add(layer); } layer.setParent(this); synchronized (IDLAYERS_WRITE_LOCK) { // Like put, but replacing the map instance final HashMap<Long,Layer> m = new HashMap<Long,Layer>(idlayers); m.put(layer.getId(), layer); idlayers = m; } synchronized (layerindices) { layerindices.clear(); } Display.updateLayerScroller(this); //debug(); } public void printDebugInfo() { Utils.log("LayerSet debug:"); for (int i=0; i<al_layers.size(); i++) Utils.log(i + " : " + al_layers.get(i).getZ()); } public Layer getParent() { return parent; } /** 'update' in database or not. */ public void setLayer(Layer layer, boolean update) { super.setLayer(layer, update); if (null != layer) this.parent = layer; // repeated pointer, eliminate 'parent' ! } public void setParent(Layer layer) { if (null == layer || layer == parent) return; this.parent = layer; updateInDatabase("parent_id"); } public void mousePressed(MouseEvent me, int x_p, int y_p, Rectangle srcRect, double mag) { if (ProjectToolbar.SELECT != ProjectToolbar.getToolId()) return; Display.setActive(me, this); if (2 == me.getClickCount() && al_layers.size() > 0) { new Display(project, al_layers.get(0)); } } public void mouseDragged(MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old, Rectangle srcRect, double mag) { if (ProjectToolbar.SELECT != ProjectToolbar.getToolId()) return; super.translate(x_d - x_d_old, y_d - y_d_old); Display.repaint(layer, this, 0); } public void mouseReleased(MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r, Rectangle srcRect, double mag) { // nothing } public void keyPressed(KeyEvent ke) { Utils.log("LayerSet.keyPressed: not yet implemented."); // TODO } public String toString() { return this.title; } public void paint(Graphics2D g, Rectangle srcRect, double magnification, boolean active, int channels, Layer active_layer) { //arrange transparency Composite original_composite = null; if (alpha != 1.0f) { original_composite = g.getComposite(); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); } // Translate graphics context accordingly AffineTransform gt = g.getTransform(); AffineTransform aff = new AffineTransform(this.at); aff.preConcatenate(gt); g.setTransform(aff); //set color g.setColor(this.color); // fill a background box g.fillRect(0, 0, (int)(this.width), (int)(this.height)); g.setColor(new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue()).brighter()); // the "opposite", but brighter, so it won't fail to generate contrast if the color is 127 in all channels int x = (int)(this.width/5); int y = (int)(this.height/5); int width = (int)(this.width/5); int height = (int)(this.height/5 * 3); g.fillRect(x, y, width, height); x = (int)(this.width/5 * 2); y = (int)(this.height/5 * 3); width = (int)(this.width/5 * 2); height = (int)(this.height/5); g.fillRect(x, y, width, height); //Transparency: fix composite back to original. if (alpha != 1.0f) { g.setComposite(original_composite); } g.setTransform(gt); } public float getLayerWidth() { return layer_width; } public float getLayerHeight() { return layer_height; } public double getRotX() { return rot_x; } public double getRotY() { return rot_y; } public double getRotZ() { return rot_z; } /** The number of Layers in this LayerSet. */ public int size() { return al_layers.size(); } public void setRotVector(double rot_x, double rot_y, double rot_z) { if (Double.isNaN(rot_x) || Double.isNaN(rot_y) || Double.isNaN(rot_z)) { Utils.showMessage("LayerSet: Rotation vector contains NaNs. Not updating."); return; } else if (rot_x == this.rot_x && rot_y == this.rot_y && rot_z == this.rot_z) { return; } this.rot_x = rot_x; this.rot_y = rot_y; this.rot_z = rot_z; updateInDatabase("rot"); } /** Used by the Loader after loading blindly a lot of Patches. Will crop the canvas to the minimum size possible. */ public boolean setMinimumDimensions() { // find current x,y,width,height that crops the canvas without cropping away any Displayable double x = Double.NaN; double y = Double.NaN; double xe = 0; // lower right corner (x end) double ye = 0; double tx = 0; double ty = 0; double txe = 0; double tye = 0; // collect all Displayable and ZDisplayable objects final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (int i=al_layers.size() -1; i>-1; i--) { al.addAll(al_layers.get(i).getDisplayables()); } al.addAll(al_zdispl); // find minimum bounding box Rectangle b = new Rectangle(); for (final Displayable d : al) { b = d.getBoundingBox(b); // considers rotation tx = b.x;//d.getX(); ty = b.y;//d.getY(); // set first coordinates if (Double.isNaN(x) || Double.isNaN(y)) { // Double.NaN == x fails! x = tx; y = ty; } txe = tx + b.width;//d.getWidth(); tye = ty + b.height;//d.getHeight(); if (tx < x) x = tx; if (ty < y) y = ty; if (txe > xe) xe = txe; if (tye > ye) ye = tye; } // if none, then stop if (Double.isNaN(x) || Double.isNaN(y)) { Utils.showMessage("No displayable objects, don't know how to resize the canvas and Layerset."); return false; } double w = xe - x; double h = ye - y; if (w <= 0 || h <= 0) { Utils.log("LayerSet.setMinimumDimensions: zero width or height, NOT resizing."); return false; } // Record previous state if (prepareStep(this)) { addEditStep(new LayerSet.DoResizeLayerSet(this)); } // translate if (0 != x || 0 != y) { project.getLoader().startLargeUpdate(); try { final AffineTransform at2 = new AffineTransform(); at2.translate(-x, -y); //Utils.log2("translating all displayables by " + x + "," + y); for (final Displayable d : al) { //((Displayable)it.next()).translate(-x, -y, false); // drag regardless of getting off current LayerSet bounds // optimized to avoid creating so many AffineTransform instances: //Utils.log2("BEFORE: " + d.getBoundingBox()); d.getAffineTransform().preConcatenate(at2); //Utils.log2("AFTER: " + d.getBoundingBox()); d.updateInDatabase("transform"); } project.getLoader().commitLargeUpdate(); } catch (Exception e) { IJError.print(e); project.getLoader().rollback(); return false; } } //Utils.log("x,y xe,ye : " + x + "," + y + " " + xe + "," + ye); // finally, accept: if (w != layer_width || h != layer_height) { this.layer_width = (float)Math.ceil(w); // stupid int to double conversions ... why floating point math is a non-solved problem? It is for SBCL this.layer_height = (float)Math.ceil(h); updateInDatabase("layer_dimensions"); recreateBuckets(true); // and notify the Displays, if any Display.update(this); Display.pack(this); } // Record current state: addEditStep(new LayerSet.DoResizeLayerSet(this)); return true; } /** Enlarge the 2D universe so that all Displayable in the collection fit in it; * that is, that no Displayable has a negative x,y position or lays beyond bounds.*/ synchronized public void enlargeToFit(final Collection<? extends Displayable> ds) { Rectangle r = null; for (Displayable d : ds) { if (null == r) r = d.getBoundingBox(); else r.add(d.getBoundingBox()); } if (null == r) return; //empty collection r.add(get2DBounds()); setDimensions(r.x, r.y, r.width, r.height); } /** Enlarges the display in the given direction; the anchor is the point to keep still, and can be any of LayerSet.NORTHWEST (top-left), etc. */ synchronized public boolean enlargeToFit(final Displayable d, final int anchor) { final Rectangle r = new Rectangle(0, 0, (int)Math.ceil(layer_width), (int)Math.ceil(layer_height)); final Rectangle b = d.getBoundingBox(null); // check if necessary if (r.contains(b)) return false; // else, enlarge to fit it r.add(b); return setDimensions(r.width, r.height, anchor); } /** May leave objects beyond the visible window. */ public void setDimensions(float x, float y, float layer_width, float layer_height) { // Record previous state if (prepareStep(this)) { addEditStep(new LayerSet.DoResizeLayerSet(this)); } this.layer_width = layer_width; this.layer_height = layer_height; final AffineTransform affine = new AffineTransform(); affine.translate(-x, -y); for (ZDisplayable zd : al_zdispl) { zd.getAffineTransform().preConcatenate(affine); zd.updateInDatabase("transform"); } for (Layer la : al_layers) la.apply(Displayable.class, affine); recreateBuckets(true); Display.update(this); // Record new state addEditStep(new LayerSet.DoResizeLayerSet(this)); } /** Returns false if any Displayables are being partially or totally cropped away. */ public boolean setDimensions(float layer_width, float layer_height, int anchor) { // check preconditions if (Double.isNaN(layer_width) || Double.isNaN(layer_height)) { Utils.log("LayerSet.setDimensions: NaNs! Not adjusting."); return false; } if (layer_width <=0 || layer_height <= 0) { Utils.showMessage("LayerSet: can't accept zero or a minus for layer width or height"); return false; } if (anchor < NORTH || anchor > CENTER) { Utils.log("LayerSet: wrong anchor, not resizing."); return false; } // Record previous state if (prepareStep(this)) { addEditStep(new LayerSet.DoResizeLayerSet(this)); } // new coordinates: double new_x = 0;// the x,y of the old 0,0 double new_y = 0; switch (anchor) { case NORTH: case SOUTH: case CENTER: new_x = (layer_width - this.layer_width) / 2; // (this.layer_width - layer_width) / 2; break; case NORTHWEST: case WEST: case SOUTHWEST: new_x = 0; break; case NORTHEAST: case EAST: case SOUTHEAST: new_x = layer_width - this.layer_width; // (this.layer_width - layer_width); break; } switch (anchor) { case WEST: case EAST: case CENTER: new_y = (layer_height - this.layer_height) / 2; break; case NORTHWEST: case NORTH: case NORTHEAST: new_y = 0; break; case SOUTHWEST: case SOUTH: case SOUTHEAST: new_y = (layer_height - this.layer_height); break; } /* Utils.log("anchor: " + anchor); Utils.log("LayerSet: existing w,h = " + this.layer_width + "," + this.layer_height); Utils.log("LayerSet: new w,h = " + layer_width + "," + layer_height); */ // collect all Displayable and ZDisplayable objects ArrayList<Displayable> al = new ArrayList<Displayable>(); for (int i=al_layers.size() -1; i>-1; i--) { al.addAll(al_layers.get(i).getDisplayables()); } al.addAll(al_zdispl); // check that no displayables are being cropped away if (layer_width < this.layer_width || layer_height < this.layer_height) { for (final Displayable d : al) { Rectangle b = d.getBoundingBox(null); double dw = b.getWidth(); double dh = b.getHeight(); // respect 10% margins if (b.x + dw + new_x < 0.1 * dw || b.x + 0.9 * dw + new_x > layer_width || b.y + dh + new_y < 0.1 * dh || b.y + 0.9 * dh + new_y > layer_height) { // cropping! Utils.showMessage("Cropping " + d + "\nLayerSet: not resizing."); return false; } } } this.layer_width = layer_width; this.layer_height = layer_height; //Utils.log("LayerSet.setDimensions: new_x,y: " + new_x + "," + new_y); // translate all displayables if (0 != new_x || 0 != new_y) { for (final Displayable d : al) { Rectangle b = d.getBoundingBox(null); //Utils.log("d x,y = " + b.x + ", " + b.y); d.setLocation(b.x + new_x, b.y + new_y); } } updateInDatabase("layer_dimensions"); recreateBuckets(true); // and notify the Display Display.update(this); Display.pack(this); // Record new state addEditStep(new LayerSet.DoResizeLayerSet(this)); return true; } protected boolean remove2(boolean check) { if (check) { if (!Utils.check("Really delete " + this.toString() + (null != al_layers && al_layers.size() > 0 ? " and all its children?" : ""))) return false; } LayerThing lt = project.findLayerThing(this); if (null == lt) return false; return project.getLayerTree().remove(check, lt, null); // will end up calling remove(boolean) on this object } public boolean remove(boolean check) { if (check) { if (!Utils.check("Really delete " + this.toString() + (null != al_layers && al_layers.size() > 0 ? " and all its children?" : ""))) return false; } // delete all layers while (0 != al_layers.size()) { if (!al_layers.get(0).remove(false)) { Utils.showMessage("LayerSet id= " + id + " : Deletion incomplete, check database."); return false; } } // delete the ZDisplayables for (final ZDisplayable zd : al_zdispl) { zd.remove(false); // will call back the LayerSet.remove(ZDisplayable) } // remove the self if (null != parent) parent.remove(this); removeFromDatabase(); return true; } /** Remove a child. Does not destroy it or delete it from the database. */ public void remove(final Layer layer) { if (null == layer || null == idlayers.get(layer.getId())) return; al_layers.remove(layer); synchronized (IDLAYERS_WRITE_LOCK) { // Like remove, but replacing the map instance final HashMap<Long,Layer> m = new HashMap<Long,Layer>(idlayers); m.remove(layer.getId()); idlayers = m; } synchronized (layerindices) { layerindices.clear(); } for (final ZDisplayable zd : new ArrayList<ZDisplayable>(al_zdispl)) zd.layerRemoved(layer); // may call back and add/remove ZDisplayable objects Display.updateLayerScroller(this); Display.updateTitle(this); removeFromOffscreens(layer); } public Layer next(final Layer layer) { final int i = indexOf(layer); if (-1 == i) { Utils.log("LayerSet.next: no such Layer " + layer); return layer; } if (al_layers.size() -1 == i) return layer; else return al_layers.get(i+1); } public Layer previous(final Layer layer) { final int i = indexOf(layer); if (-1 == i) { Utils.log("LayerSet.previous: no such Layer " + layer); return layer; } if (0 == i) return layer; else return al_layers.get(i-1); } public Layer nextNonEmpty(Layer layer) { Layer next = layer; Layer given = layer; do { layer = next; next = next(layer); if (!next.isEmpty()) return next; } while (next != layer); return given; } public Layer previousNonEmpty(Layer layer) { Layer previous = layer; Layer given = layer; do { layer = previous; previous = previous(layer); if (!previous.isEmpty()) return previous; } while (previous != layer); return given; } public int getLayerIndex(final long id) { final Layer layer = getLayer(id); if (null == layer) return -1; return indexOf(layer); } /** Find a layer by index, or null if none. */ public Layer getLayer(final int i) { if (i >=0 && i < al_layers.size()) return al_layers.get(i); return null; } /** Find a layer with the given id, or null if none. */ public Layer getLayer(final long id) { return idlayers.get(id); } /** Same as getLayer(long) but without box/unbox. */ public Layer getLayer(final Long id) { return idlayers.get(id); } /** Returns the first layer found with the given Z coordinate, rounded to seventh decimal precision, or null if none found. */ public Layer getLayer(final double z) { double error = 0.0000001; // TODO adjust to an optimal for (Layer layer : al_layers) { if (error > Math.abs(layer.getZ() - z)) { // floating-point arithmetic is still not a solved problem! return layer; } } return null; } public Layer getNearestLayer(final double z) { double min_dist = Double.MAX_VALUE; Layer closest = null; for (Layer layer : al_layers) { double dist = Math.abs(layer.getZ() - z); if (dist < min_dist) { min_dist = dist; closest = layer; } } return closest; } /** Returns null if none has the given z and thickness. If 'create' is true and no layer is found, a new one with the given Z is created and added to the LayerTree. */ public Layer getLayer(double z, double thickness, boolean create) { Iterator<Layer> it = al_layers.iterator(); Layer layer = null; double error = 0.0000001; // TODO adjust to an optimal while (it.hasNext()) { Layer l = it.next(); if (error > Math.abs(l.getZ() - z) && error > Math.abs(l.getThickness() - thickness)) { // floating point is still not a solved problem. //Utils.log("LayerSet.getLayer: found layer with z=" + l.getZ()); layer = l; } } if (create && null == layer && !Double.isNaN(z) && !Double.isNaN(thickness)) { //Utils.log("LayerSet.getLayer: creating new Layer with z=" + z); layer = new Layer(project, z, thickness, this); add(layer); project.getLayerTree().addLayer(this, layer); } return layer; } /** Useful for typeless scripts so that a ZDisplayable can be added; the * overloaded method add(Layer) and add(ZDisplayable) is not distinguishable otherwise. */ public void addZDisplayable(final ZDisplayable zdispl) { add(zdispl); } /** Add a Displayable to be painted in all Layers, such as a Pipe. Also updates open displays of the fact. */ public void add(final ZDisplayable zdispl) { if (null == zdispl || -1 != al_zdispl.indexOf(zdispl)) { Utils.log2("LayerSet: not adding zdispl"); return; } if (zdispl.getProject() != this.project) throw new IllegalArgumentException("LayerSet rejected a ZDisplayable: belongs to a different project."); al_zdispl.add(zdispl); // at the top zdispl.setLayerSet(this); // The line below can fail (and in the addSilently as well) if one can add zdispl objects while no Layer has been created. But the ProjectThing.createChild prevents this situation. zdispl.setLayer(al_layers.get(0)); zdispl.updateInDatabase("layer_set_id"); // TODO: update stack index? It should! // insert into bucket /* if (null != root) { // add as last, then update root.put(al_zdispl.size()-1, zdispl, zdispl.getBoundingBox(null)); // Updating takes too long, just don't do it //root.update(this, zdispl, 0, al_zdispl.size()-1); } */ addToBuckets(zdispl, al_zdispl.size()-1); Display.add(this, zdispl); } public void addAll(final Collection<? extends ZDisplayable> coll) { if (null == coll || 0 == coll.size()) return; for (final ZDisplayable zd : coll) { al_zdispl.add(zd); zd.setLayerSet(this); zd.setLayer(al_layers.get(0)); zd.updateInDatabase("layer_set_id"); } recreateBuckets(false); // only ZDisplayable Display.addAll(this, coll); } /** Used for reconstruction purposes, avoids repainting or updating. */ public void addSilently(final ZDisplayable zdispl) { if (null == zdispl || -1 != al_zdispl.indexOf(zdispl)) return; try { zdispl.setLayer(0 == al_layers.size() ? null : al_layers.get(0)); zdispl.setLayerSet(this, false); //Utils.log2("setLayerSet to ZDipl id=" + zdispl.getId()); al_zdispl.add(zdispl); } catch (Exception e) { Utils.log("LayerSet.addSilently: not adding ZDisplayable with id=" + zdispl.getId()); IJError.print(e); return; } } /** Remove a child. Does not destroy the child nor remove it from the database, only from the LayerSet and the Display. */ public boolean remove(final ZDisplayable zdispl) { if (null == zdispl || null == al_zdispl) return false; final int old_stack_index = al_zdispl.indexOf(zdispl); if (-1 == old_stack_index) { Utils.log2("LayerSet.remove: Not found: " + zdispl); return false; } al_zdispl.remove(old_stack_index); // remove from Bucket AFTER modifying stack index, so it gets reindexed properly removeFromBuckets(zdispl, old_stack_index); removeFromOffscreens(zdispl); Display.remove(zdispl); return true; } /** Remove a child. Does not destroy the child nor remove it from the database, only from the LayerSet and the Display. * Returns false if at least one failed to be removed. */ public boolean removeAll(final Set<ZDisplayable> zds) { if (null == zds || null == al_zdispl) return false; // Ensure list is iterated only once: don't ask for index every time! int count = 0; for (final Iterator<ZDisplayable> it = al_zdispl.iterator(); it.hasNext(); ) { final ZDisplayable zd = it.next(); if (zds.contains(zd)) { it.remove(); removeFromOffscreens(zd); Display.remove(zd); count++; if (zds.size() == count) break; } } removeFromBuckets(zds); Display.updateVisibleTabs(this.project); return true; } public boolean contains(final Layer layer) { if (null == layer) return false; return -1 != indexOf(layer); } public boolean contains(final Displayable zdispl) { if (null == zdispl) return false; return -1 != al_zdispl.indexOf(zdispl); } /** Returns a copy of the layer list. */ public ArrayList<Layer> getLayers() { return new ArrayList<Layer>(al_layers); // for integrity and safety, return a copy. } /** Returns a sublist of layers from first to last, both inclusive. If last is larger than first, the order is reversed. */ public List<Layer> getLayers(final int first, final int last) { final List<Layer> las = al_layers.subList(Math.min(first, last), Math.max(first, last) +1); if (first > last) { final List<Layer> las2 = new ArrayList<Layer>(las); Collections.reverse(las2); // would otherwise reverse the original list! A monumental error. return las2; } return new ArrayList<Layer>(las); // editable, thread-safe (a copy) } /** Returns the layer range from first to last, both included. If last.getZ() < first.getZ(), the order is reversed. */ public List<Layer> getLayers(final Layer first, final Layer last) { return getLayers(indexOf(first), indexOf(last)); } /** Returns the list of layers to paint by considering the range of n_layers_color_cue around the active layer index. */ public List<Layer> getColorCueLayerRange(final Layer active_layer) { if (n_layers_color_cue < 0) { return new ArrayList<Layer>(al_layers); // a copy of all } else if (0 == n_layers_color_cue) { final ArrayList<Layer> list = new ArrayList<Layer>(); list.add(active_layer); return list; } // Else: final int i = indexOf(active_layer); if (-1 == i) { Utils.log("An error ocurred: could not find an index for layer " + active_layer); final ArrayList<Layer> a = new ArrayList<Layer>(); a.add(active_layer); return a; } int first = i - n_layers_color_cue; int last = i + n_layers_color_cue; if (first < 0) first = 0; int size = al_layers.size(); if (last >= size) last = size -1; return getLayers(first, last); } public boolean isDeletable() { return false; } /** Overiding. The alpha is used to show whether the LayerSet object is selected or not. */ public void setAlpha(float alpha) { return; } /** Move the given Displayable to the next layer if possible. */ public void moveDown(final Layer layer, final Displayable d) { final int i = indexOf(layer); if (al_layers.size() -1 == i || -1 == i) return; layer.remove(d); (al_layers.get(i +1)).add(d); } /** Move the given Displayable to the previous layer if possible. */ public void moveUp(final Layer layer, final Displayable d) { final int i = indexOf(layer); if (0 == i || -1 == i) return; layer.remove(d); al_layers.get(i -1).add(d); } /** Move all Displayable objects in the HashSet to the given target layer. */ public void move(final Set<Displayable> hs_d, final Layer source, final Layer target) { if (0 == hs_d.size() || null == source || null == target || source == target) return; Display.setRepaint(false); // disable repaints for (final Displayable d : hs_d) { if (d instanceof ZDisplayable) continue; // ignore if (source == d.getLayer()) { source.remove(d); target.add(d, false, false); // these contortions to avoid repeated DB traffic d.updateInDatabase("layer_id"); Display.add(target, d, false); // don't activate } } Display.setRepaint(true); // enable repaints source.updateInDatabase("stack_index"); target.updateInDatabase("stack_index"); Display.repaint(source); // update graphics: true Display.repaint(target); } /** Returns the hash set of objects whose visibility has changed. */ public HashSet<Displayable> setVisible(String type, final boolean visible, final boolean repaint) { type = type.toLowerCase(); final HashSet<Displayable> hs = new HashSet<Displayable>(); try { project.getLoader().startLargeUpdate(); if (type.equals("connector") || type.equals("treeline") || type.equals("areatree") || type.equals("pipe") || type.equals("ball") || type.equals("arealist") || type.equals("polyline") || type.equals("stack") || type.equals("dissector")) { for (ZDisplayable zd : al_zdispl) { if (visible != zd.isVisible() && zd.getClass().getName().toLowerCase().endsWith(type)) { // endsWith, because DLabel is called as Label zd.setVisible(visible, false); // don't repaint hs.add(zd); } } } else { for (Layer layer : al_layers) { hs.addAll(layer.setVisible(type, visible, false)); // don't repaint } } } catch (Exception e) { IJError.print(e); } finally { project.getLoader().commitLargeUpdate(); } if (repaint) { Display.repaint(this); // this could be optimized to repaint only the accumulated box } return hs; } /** Hide all except those whose type is in 'type' list, whose visibility flag is left unchanged. Returns the list of displayables made hidden. */ public HashSet<Displayable> hideExcept(ArrayList<Class<?>> type, boolean repaint) { final HashSet<Displayable> hs = new HashSet<Displayable>(); for (ZDisplayable zd : al_zdispl) { if (!type.contains(zd.getClass()) && zd.isVisible()) { zd.setVisible(false, repaint); hs.add(zd); } } for (Layer la : al_layers) hs.addAll(la.hideExcept(type, repaint)); return hs; } /** Returns the collection of Displayable whose visibility state has changed. */ public Collection<Displayable> setAllVisible(final boolean repaint) { final Collection<Displayable> col = new ArrayList<Displayable>(); for (final ZDisplayable zd : al_zdispl) { if (!zd.isVisible()) { zd.setVisible(true, repaint); col.add(zd); } } for (Layer la : al_layers) col.addAll(la.setAllVisible(repaint)); return col; } /** Returns true if any of the ZDisplayable objects are of the given class. */ public boolean contains(final Class<?> c) { for (final ZDisplayable zd : al_zdispl) { if (zd.getClass() == c) return true; } return false; } /** Check in all layers. */ public boolean containsDisplayable(final Class<?> c) { for (final Layer layer : al_layers) { if (layer.contains(c)) return true; } return false; } /** Returns the distance from the first layer's Z to the last layer's Z. */ public double getDepth() { if (null == al_layers || al_layers.isEmpty()) return 0; return al_layers.get(al_layers.size() -1).getZ() - al_layers.get(0).getZ(); } /** Return all the Displayable objects from all the layers of this LayerSet. Does not include the ZDisplayables. */ public ArrayList<Displayable> getDisplayables() { final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (Layer layer : al_layers) { al.addAll(layer.getDisplayables()); } return al; } /** Return all the Displayable objects from all the layers of this LayerSet of the given class. Does not include the ZDisplayables. */ public ArrayList<Displayable> getDisplayables(Class<?> c) { final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (Layer layer : al_layers) { al.addAll(layer.getDisplayables(c)); } return al; } /** Return all the Displayable objects from all the layers of this LayerSet of the given class that intersect the given area. Does not include the ZDisplayables. */ public ArrayList<Displayable> getDisplayables(final Class<?> c, final Area aroi, final boolean visible_only) { final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (Layer layer : al_layers) { al.addAll(layer.getDisplayables(c, aroi, visible_only)); } return al; } /** From zero to size-1. */ public int indexOf(final Layer layer) { synchronized (layerindices) { Integer i = layerindices.get(layer); if (null == i) { // Recreate layerindices.clear(); int k = 0; for (final Layer la : al_layers) { layerindices.put(la, k); k++; } i = layerindices.get(layer); if (null == i) { Utils.log("ERROR: could not find an index for layer " + layer); return -1; } } return i.intValue(); } } private static java.lang.reflect.Field sbvalue = null; static { try { sbvalue = StringBuilder.class.getSuperclass().getDeclaredField("value"); sbvalue.setAccessible(true); } catch (Exception e) { IJError.print(e); } } public void exportXML(final java.io.Writer writer, final String indent, final XMLOptions options) throws Exception { final StringBuilder sb_body = new StringBuilder(512); sb_body.append(indent).append("<t2_layer_set\n"); final String in = indent + "\t"; super.exportXML(sb_body, in, options); sb_body.append(in).append("layer_width=\"").append(layer_width).append("\"\n") .append(in).append("layer_height=\"").append(layer_height).append("\"\n") .append(in).append("rot_x=\"").append(rot_x).append("\"\n") .append(in).append("rot_y=\"").append(rot_y).append("\"\n") .append(in).append("rot_z=\"").append(rot_z).append("\"\n") .append(in).append("snapshots_quality=\"").append(snapshots_quality).append("\"\n") .append(in).append("snapshots_mode=\"").append(snapshot_modes[snapshots_mode]).append("\"\n") .append(in).append("color_cues=\"").append(color_cues).append("\"\n") .append(in).append("area_color_cues=\"").append(area_color_cues).append("\"\n") .append(in).append("avoid_color_cue_colors=\"").append(!use_color_cue_colors).append("\"\n") .append(in).append("n_layers_color_cue=\"").append(n_layers_color_cue).append("\"\n") .append(in).append("paint_arrows=\"").append(paint_arrows).append("\"\n") .append(in).append("paint_tags=\"").append(paint_tags).append("\"\n") .append(in).append("paint_edge_confidence_boxes=\"").append(paint_edge_confidence_boxes).append("\"\n") .append(in).append("prepaint=\"").append(prepaint).append("\"\n") .append(in).append("preload_ahead=\"").append(preload_ahead).append("\"\n") // TODO: alpha! But it's not necessary. ; sb_body.append(indent).append(">\n"); if (null != calibration) { sb_body.append(in).append("<t2_calibration\n") .append(in).append("\tpixelWidth=\"").append(calibration.pixelWidth).append("\"\n") .append(in).append("\tpixelHeight=\"").append(calibration.pixelHeight).append("\"\n") .append(in).append("\tpixelDepth=\"").append(calibration.pixelDepth).append("\"\n") .append(in).append("\txOrigin=\"").append(calibration.xOrigin).append("\"\n") .append(in).append("\tyOrigin=\"").append(calibration.yOrigin).append("\"\n") .append(in).append("\tzOrigin=\"").append(calibration.zOrigin).append("\"\n") .append(in).append("\tinfo=\"").append(calibration.info).append("\"\n") .append(in).append("\tvalueUnit=\"").append(calibration.getValueUnit()).append("\"\n") .append(in).append("\ttimeUnit=\"").append(calibration.getTimeUnit()).append("\"\n") .append(in).append("\tunit=\"").append(calibration.getUnit()).append("\"\n") .append(in).append("/>\n") ; } if (null == sbvalue) { writer.write(sb_body.toString()); } else { writer.write((char[])sbvalue.get(sb_body), 0, sb_body.length()); // avoid making a copy of the array } // Count objects int done = 0; int total = 0; total += al_zdispl.size(); for (final Layer la : al_layers) { total += la.getDisplayableList().size(); } // export ZDisplayable objects if (null != al_zdispl) { for (final ZDisplayable zd : al_zdispl) { sb_body.setLength(0); zd.exportXML(sb_body, in, options); if (null == sbvalue) { writer.write(sb_body.toString()); // each separately, for they can be huge } else { writer.write((char[])sbvalue.get(sb_body), 0, sb_body.length()); // avoid making a copy of the array } } done += al_zdispl.size(); Utils.showProgress(done / (double)total); } // export Layer and contained Displayable objects if (null != al_layers) { //Utils.log("LayerSet " + id + " is saving " + al_layers.size() + " layers."); for (final Layer la : al_layers) { sb_body.setLength(0); la.exportXML(sb_body, in, options); if (null == sbvalue) { writer.write(sb_body.toString()); } else { writer.write((char[])sbvalue.get(sb_body), 0, sb_body.length()); // avoid making a copy of the array } done += la.getDisplayableList().size(); Utils.showProgress(done / (double)total); } } sb_body.setLength(0); if (sb_body.length() > 0) { super.restXML(sb_body, in, options); if (null == sbvalue) { writer.write(sb_body.toString()); } else { writer.write((char[])sbvalue.get(sb_body), 0, sb_body.length()); // avoid making a copy of the array } } writer.write(indent + "</t2_layer_set>\n"); } /** Includes the !ELEMENT */ static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) { final String type = "t2_layer_set"; if (!hs.contains(type)) { sb_header.append(indent).append("<!ELEMENT t2_layer_set (").append(Displayable.commonDTDChildren()).append(",t2_layer,t2_pipe,t2_ball,t2_area_list,t2_calibration,t2_stack,t2_treeline)>\n"); Displayable.exportDTD(type, sb_header, hs, indent); sb_header.append(indent).append(TAG_ATTR1).append(type).append(" layer_width").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" layer_height").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" rot_x").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" rot_y").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" rot_z").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" snapshots_quality").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" color_cues").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" area_color_cues").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" avoid_color_cue_colors").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" n_layers_color_cue").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" paint_arrows").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" paint_tags").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" paint_edge_confidence_boxes").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append(type).append(" preload_ahead").append(TAG_ATTR2) ; sb_header.append(indent).append("<!ELEMENT t2_calibration EMPTY>\n") .append(indent).append(TAG_ATTR1).append("t2_calibration pixelWidth").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration pixelHeight").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration pixelDepth").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration xOrigin").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration yOrigin").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration zOrigin").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration info").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration valueUnit").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration timeUnit").append(TAG_ATTR2) .append(indent).append(TAG_ATTR1).append("t2_calibration unit").append(TAG_ATTR2) ; } } public void setSnapshotsMode(final int mode) { if (mode == snapshots_mode) return; this.snapshots_mode = mode; Display.repaintSnapshots(this); updateInDatabase("snapshots_mode"); } public int getSnapshotsMode() { return this.snapshots_mode; } @Override public void destroy() { for (Iterator<Layer> it = al_layers.iterator(); it.hasNext(); ) { Layer layer = it.next(); layer.destroy(); } for (final ZDisplayable zd : al_zdispl) { zd.destroy(); } this.al_layers.clear(); this.al_zdispl.clear(); synchronized (IDLAYERS_WRITE_LOCK) { this.idlayers = new HashMap<Long,Layer>(); } // like .clear() synchronized (layerindices) { this.layerindices.clear(); } this.offscreens.clear(); } /** Used by the Layer.setZ method. */ protected void reposition(final Layer layer) { if (null == layer || !idlayers.containsKey(layer.getId())) return; al_layers.remove(layer); addSilently(layer); } /** Get up to 'n' layers before and after the given layers. */ public ArrayList<Layer> getNeighborLayers(final Layer layer, final int n) { final int i_layer = indexOf(layer); final ArrayList<Layer> al = new ArrayList<Layer>(); if (-1 == i_layer) return al; int start = i_layer - n; if (start < 0) start = 0; int end = i_layer + n; if (end > al_layers.size()) end = al_layers.size(); for (int i=start; i<i_layer; i++) al.add(al_layers.get(i)); for (int i=i_layer+1; i<= i_layer + n || i < end; i++) al.add(al_layers.get(i)); return al; } public boolean isTop(ZDisplayable zd) { if (null != zd && al_zdispl.size() > 0 && al_zdispl.indexOf(zd) == al_zdispl.size() -1) return true; return false; } public boolean isBottom(ZDisplayable zd) { if (null != zd && al_zdispl.size() > 0 && al_zdispl.indexOf(zd) == 0) return true; return false; } /** Hub method: ZDisplayable or into the Displayable's Layer. */ protected boolean isTop(Displayable d) { if (d instanceof ZDisplayable) return isTop((ZDisplayable)d); else return d.getLayer().isTop(d); } /** Hub method: ZDisplayable or into the Displayable's Layer. */ protected boolean isBottom(Displayable d) { if (d instanceof ZDisplayable) return isBottom((ZDisplayable)d); else return d.getLayer().isBottom(d); } /** Change z position in the layered stack, which defines the painting order. */ // the BOTTOM of the stack is the first element in the al_zdispl array synchronized protected void move(final int place, final Displayable d) { if (d instanceof ZDisplayable) { int i = al_zdispl.indexOf(d); if (-1 == i) { Utils.log("LayerSet.move: object does not belong here"); return; } int size = al_zdispl.size(); if (1 == size) return; switch(place) { case LayerSet.TOP: // To the end of the list: al_zdispl.add(al_zdispl.remove(i)); // OLD // if (null != root) root.update(this, d, i, al_zdispl.size()-1); updateRangeInBuckets(d, i, al_zdispl.size()-1); break; case LayerSet.UP: // +1 in the list if (size -1 == i) return; al_zdispl.add(i+1, al_zdispl.remove(i)); //if (null != root) root.update(this, d, i, i+1); updateRangeInBuckets(d, i, i+1); break; case LayerSet.DOWN: // -1 in the list if (0 == i) return; al_zdispl.add(i-1, al_zdispl.remove(i)); //swap //if (null != root) root.update(this, d, i-1, i); updateRangeInBuckets(d, i-1, i); break; case LayerSet.BOTTOM: // to first position in the list al_zdispl.add(0, al_zdispl.remove(i)); //if (null != root) root.update(this, d, 0, i); updateRangeInBuckets(d, 0, i); break; } updateInDatabase("stack_index"); Display.updatePanelIndex(d.getLayer(), d); } else { switch (place) { case LayerSet.TOP: d.getLayer().moveTop(d); break; case LayerSet.UP: d.getLayer().moveUp(d); break; case LayerSet.DOWN: d.getLayer().moveDown(d); break; case LayerSet.BOTTOM: d.getLayer().moveBottom(d); break; } } } /** Returns the reverse index of ZDisplayable zd, which is the actual index as seen in the screen. */ public int indexOf(final ZDisplayable zd) { int k = al_zdispl.indexOf(zd); if (-1 == k) return -1; return al_zdispl.size() - k -1; } public boolean isEmptyAt(final Layer layer) { for (final ZDisplayable zd : al_zdispl) { if (zd.paintsAt(layer)) return false; } return true; } public Displayable clone(final Project pr, final boolean copy_id) { final Rectangle roi = new Rectangle(0, 0, (int)Math.ceil(getLayerWidth()), (int)Math.ceil(getLayerHeight())); final LayerSet copy = (LayerSet) clone(pr, al_layers.get(0), al_layers.get(al_layers.size()-1), roi, false, copy_id, false); try { LayerSet.cloneInto(this, al_layers.get(0), al_layers.get(al_layers.size()-1), pr, copy, roi, copy_id); } catch (Exception e) { IJError.print(e); return null; } return copy; } /** Clone the contents of this LayerSet, from first to last given layers, and cropping for the given rectangle; * does NOT copy the ZDisplayable, which may be copied using the LayerSet.cloneInto method. */ public Displayable clone(Project pr, Layer first, Layer last, Rectangle roi, boolean add_to_tree, boolean copy_id, boolean ignore_hidden_patches) { // obtain a LayerSet final long nid = copy_id ? this.id : pr.getLoader().getNextId(); final LayerSet copy = new LayerSet(pr, nid, getTitle(), this.width, this.height, this.rot_x, this.rot_y, this.rot_z, roi.width, roi.height, this.locked, this.snapshots_mode, (AffineTransform)this.at.clone()); copy.setCalibration(getCalibrationCopy()); copy.snapshots_quality = this.snapshots_quality; // copy objects that intersect the roi, from within the given range of layers final java.util.List<Layer> range = new ArrayList<Layer>(al_layers).subList(indexOf(first), indexOf(last) +1); Utils.log2("range.size() : " + range.size()); for (Layer layer : range) { Layer layercopy = layer.clone(pr, copy, roi, copy_id, ignore_hidden_patches); copy.addSilently(layercopy); if (add_to_tree) pr.getLayerTree().addLayer(copy, layercopy); } return copy; } static public void cloneInto(final LayerSet src, Layer src_first, Layer src_last, final Project pr, final LayerSet copy, Rectangle roi, boolean copy_id) throws Exception { // copy ZDisplayable objects if they intersect the roi, and translate them properly final AffineTransform trans = new AffineTransform(); trans.translate(-roi.x, -roi.y); final List<Layer> range = copy.getLayers(); List<Layer> src_range = null; if (0 == range.size()) throw new Exception("Cannot cloneInto for a range of zero layers!"); for (final ZDisplayable zd : src.find(range.get(0), range.get(range.size()-1), new Area(roi))) { if (src.project != pr && zd instanceof Tree<?>) { // Special in-cloning + crop + out-cloning for Tree instances, since they hold Layer pointers // 1. Clone within same project, with ALL layers present ZDisplayable src_zd_copy = (ZDisplayable)zd.clone(src.project, true); // NOTICE I use src.project, not pr! And also reuse same id -- this is a throwaway. // 2. Crop to the desired range, using the range from the original project if (null == src_range) src_range = new ArrayList<Layer>(src.al_layers).subList(src.indexOf(src_first), src.indexOf(src_last) +1); src_zd_copy.crop(src_range); // 3. Clone the cropped Tree Tree<?> tcopy = (Tree<?>)src_zd_copy.clone(pr, copy_id); tcopy.getAffineTransform().preConcatenate(trans); copy.addSilently(tcopy); tcopy.calculateBoundingBox(null); // for all layers. Will update buckets. continue; } // Else, normally: ZDisplayable zdcopy = (ZDisplayable)zd.clone(pr, copy_id); zdcopy.getAffineTransform().preConcatenate(trans); copy.addSilently(zdcopy); // must be added before attempting to crop it, because crop needs a LayerSet ref. if (zdcopy.crop(range)) { if (zdcopy.isDeletable()) { pr.remove(zdcopy); Utils.log("Skipping empty " + zdcopy); } else { zdcopy.calculateBoundingBox(null); // null means update buckets for all layers } } else { Utils.log("Could not crop " + zd); } } // fix links: copy.linkPatchesR(); } /** Create a virtual layer stack that acts as a virtual ij.ImageStack, in RGB and set to a scale of max_dimension / Math.max(layer_width, layer_height). */ public LayerStack createLayerStack(Class<?> clazz, int type, int c_alphas) { return new LayerStack(this, getVirtualizationScale(), type, clazz, c_alphas); } public int getPixelsMaxDimension() { return max_dimension; } /** From 0.000... to 1. */ public double getVirtualizationScale() { double scale = max_dimension / Math.max(layer_width, layer_height); return scale > 1 ? 1 : scale; } public void setPixelsMaxDimension(final int d) { if (d > 2) { if (d != max_dimension) { max_dimension = d; Polyline.flushTraceCache(project); // depends on the scale value Utils.log("3D Viewer NOT updated:\n close it and recreate meshes for any objects you had in it."); } } else Utils.log("Can't set virtualization max pixels dimension to smaller than 2!"); } public void setPixelsVirtualizationEnabled(boolean b) { this.virtualization_enabled = b; } public boolean isPixelsVirtualizationEnabled() { return virtualization_enabled; } /** Returns a new Rectangle of 0, 0, layer_width, layer_height. */ public Rectangle get2DBounds() { return new Rectangle(0, 0, (int)Math.ceil(layer_width), (int)Math.ceil(layer_height)); } /** Set the calibration to a clone of the given calibration. */ public void setCalibration(Calibration cal) { if (null == cal) return; this.calibration = (Calibration)cal.clone(); } public Calibration getCalibration() { return this.calibration; } public Calibration getCalibrationCopy() { return calibration.copy(); } public boolean isCalibrated() { Calibration identity = new Calibration(); if (identity.equals(this.calibration)) return false; return true; } /** Restore calibration from the given XML attributes table.*/ public void restoreCalibration(HashMap<String,String> ht_attributes) { for (final Map.Entry<String,String> entry : ht_attributes.entrySet()) { final String key = (String)entry.getKey(); final String value = (String)entry.getValue(); // remove the prefix 't2_' key.substring(3).toLowerCase(); // case-resistant try { if (key.equals("pixelwidth")) { calibration.pixelWidth = Double.parseDouble(value); } else if (key.equals("pixelheight")) { calibration.pixelHeight = Double.parseDouble(value); } else if (key.equals("pixeldepth")) { calibration.pixelDepth = Double.parseDouble(value); } else if (key.equals("xorigin")) { calibration.xOrigin = Double.parseDouble(value); } else if (key.equals("yorigin")) { calibration.yOrigin = Double.parseDouble(value); } else if (key.equals("zorigin")) { calibration.zOrigin = Double.parseDouble(value); } else if (key.equals("info")) { calibration.info = value; } else if (key.equals("valueunit")) { calibration.setValueUnit(value); } else if (key.equals("timeunit")) { calibration.setTimeUnit(value); } else if (key.equals("unit")) { calibration.setUnit(value); } } catch (Exception e) { Utils.log2("LayerSet.restoreCalibration, key/value failed:" + key + "=\"" + value +"\""); IJError.print(e); } } //Utils.log2("Restored LayerSet calibration: " + calibration); } /** For creating snapshots, using a very slow but much better scaling algorithm (the Image.SCALE_AREA_AVERAGING method). */ public boolean snapshotsQuality() { return snapshots_quality; } public void setSnapshotsQuality(boolean b) { this.snapshots_quality = b; updateInDatabase("snapshots_quality"); // TODO this is obsolete } /** Find, in this LayerSet and contained layers and their nested LayerSets if any, all Displayable instances of Class c. Includes the ZDisplayables. */ public ArrayList<Displayable> get(final Class<?> c) { return get(new ArrayList<Displayable>(), c); } /** Find, in this LayerSet and contained layers and their nested LayerSets if any, all Displayable instances of Class c, which are stored in the given ArrayList; returns the same ArrayList, or a new one if its null. Includes the ZDisplayables. */ public ArrayList<Displayable> get(ArrayList<Displayable> all, final Class<?> c) { if (null == all) all = new ArrayList<Displayable>(); // check whether to include all the ZDisplayable objects if (Displayable.class == c || ZDisplayable.class == c) all.addAll(al_zdispl); else { for (final ZDisplayable zd : al_zdispl) { if (zd.getClass() == c) all.add(zd); } } for (final Layer layer : al_layers) { all.addAll(layer.getDisplayables(c)); for (final Displayable ls : layer.getDisplayables(LayerSet.class)) { ((LayerSet)ls).get(all, c); } } return all; } /** Returns the region defined by the rectangle as an image in the type and format specified. * The type is either ImagePlus.GRAY8 or ImagePlus.COLOR_RGB. * The format is either Layer.IMAGE (an array) or Layer.ImagePlus (it returns an ImagePlus containing an ImageStack), from which any ImageProcessor or pixel arrays can be retrieved trivially. */ public Object grab(final int first, final int last, final Rectangle r, final double scale, final Class<?> c, final int c_alphas, final int format, final int type) { // check preconditions if (first < 0 || first > last || last >= al_layers.size()) { Utils.log("Invalid first and/or last layers."); return null; } // Ensure some memory is free project.getLoader().releaseToFit(r.width, r.height, type, 1.1f); if (Layer.IMAGEPLUS == format) { ImageStack stack = new ImageStack((int)(r.width*scale), (int)(r.height*scale)); for (int i=first; i<=last; i++) { Layer la = al_layers.get(i); Utils.log2("c is " + c); ImagePlus imp = project.getLoader().getFlatImage(la, r, scale, c_alphas, type, c, null, true); if (null != imp) try { //if (0 == stack.getSize()) stack.setColorModel(imp.getProcessor().getColorModel()); stack.addSlice(imp.getTitle(), imp.getProcessor()); //.getPixels()); } catch (IllegalArgumentException iae) { IJError.print(iae); } else Utils.log("LayerSet.grab: Ignoring layer " + la); } if (0 == stack.getSize()) { Utils.log("LayerSet.grab: could not make slices."); return null; } return new ImagePlus("Stack " + first + "-" + last, stack); } else if (Layer.IMAGE == format) { final Image[] image = new Image[last - first + 1]; for (int i=first, j=0; i<=last; i++, j++) { image[j] = project.getLoader().getFlatAWTImage(al_layers.get(i), r, scale, c_alphas, type, c, null, true, Color.black); } return image; } return null; } /** Searches in all layers. Ignores the ZDisplaybles. */ public Displayable findDisplayable(final long id) { for (Layer la : al_layers) { for (Displayable d : la.getDisplayables()) { if (d.getId() == id) return d; } } return null; } /** Searches in all ZDisplayables and in all layers, recursively into nested LayerSets. */ public DBObject findById(final long id) { if (this.id == id) return this; for (ZDisplayable zd : al_zdispl) { if (zd.getId() == id) return zd; } for (Layer la : al_layers) { DBObject dbo = la.findById(id); if (null != dbo) return dbo; } return null; } // private to the package void linkPatchesR() { for (Layer la : al_layers) la.linkPatchesR(); for (ZDisplayable zd : al_zdispl) zd.linkPatches(); } /** Recursive into nested LayerSet objects.*/ public void updateLayerTree() { for (Layer la : al_layers) { la.updateLayerTree(); } } /** Find the ZDisplayable objects that intersect with the 3D roi defined by the first and last layers, and the area -all in world coordinates. */ public ArrayList<ZDisplayable> find(final Layer first, final Layer last, final Area area) { final ArrayList<ZDisplayable> al = new ArrayList<ZDisplayable>(); for (ZDisplayable zd : al_zdispl) { if (zd.intersects(area, first.getZ(), last.getZ())) { al.add(zd); } } return al; } /** A Bucket for the ZDisplayable parts that show in every Layer. */ protected final class LayerBucket { protected final Bucket root; protected final HashMap<Displayable,HashSet<Bucket>> db_map = new HashMap<Displayable,HashSet<Bucket>>(); LayerBucket(final Layer la) { this.root = new Bucket(0, 0, (int)(0.00005 + getLayerWidth()), (int)(0.00005 + getLayerHeight()), Bucket.getBucketSide(LayerSet.this, la)); this.root.populate(LayerSet.this, la, this.db_map); } } /** For fast search. */ protected HashMap<Layer,LayerBucket> lbucks = new HashMap<Layer,LayerBucket>(); final private void addToBuckets(final Displayable zd, final int i) { synchronized (lbucks) { if (lbucks.isEmpty()) return; for (final Long lid : zd.getLayerIds()) { final Layer la = getLayer(lid); // map lookup final LayerBucket lb = lbucks.get(la); if (null == lb) { nbmsg(la); continue; } lb.root.put(i, zd, la, lb.db_map); } } } /** Recreate the buckets of every layer in which the {@link Displayable} has data. */ final private void removeFromBuckets(final Displayable zd, final int old_stack_index) { synchronized (lbucks) { if (lbucks.isEmpty()) return; for (final Long lid : zd.getLayerIds()) { final Layer la = getLayer(lid); final LayerBucket lb = lbucks.get(la); if (null == lb) { nbmsg(la); continue; } recreateBuckets(getLayer(lid), false); } } } /** Recreate the buckets for all layers involved. */ final private void removeFromBuckets(final Collection<ZDisplayable> zds) { synchronized (lbucks) { if (lbucks.isEmpty()) return; final Set<Layer> touched = new HashSet<Layer>(); for (final ZDisplayable zd : zds) { touched.addAll(zd.getLayersWithData()); } for (final Layer la : touched) { final LayerBucket lb = lbucks.remove(la); if (null == lb) { nbmsg(la); continue; } lbucks.put(la, new LayerBucket(la)); } } } /** Used ONLY by move up/down/top/bottom. */ final private void updateRangeInBuckets(final Displayable zd, final int i, final int j) { synchronized (lbucks) { if (lbucks.isEmpty()) return; for (final Long lid : zd.getLayerIds()) { final Layer la = getLayer(lid); final LayerBucket lb = lbucks.get(la); if (null == lb) { nbmsg(la); continue; } for (final Bucket bu : lb.db_map.get(zd)) { bu.updateRange(this, zd, i, j); } } } } /** Returns a copy of the list of ZDisplayable objects. */ public ArrayList<ZDisplayable> getZDisplayables() { return new ArrayList<ZDisplayable>(al_zdispl); } /** Returns the real list of displayables, not a copy. If you modify this list, Thor may ground you with His lightning. */ public ArrayList<ZDisplayable> getDisplayableList() { return al_zdispl; } public HashMap<Displayable, HashSet<Bucket>> getBucketMap(final Layer la) { synchronized (lbucks) { if (lbucks.isEmpty()) return null; final LayerBucket lb = lbucks.get(la); if (null == lb) { nbmsg(la); return null; } return lb.db_map; } } public void updateBucket(final Displayable d, final Layer layer) { synchronized (lbucks) { final LayerBucket lb = lbucks.get(layer); if (null != lb) lb.root.updatePosition(d, layer, lb.db_map); } } /** Recreate the ZDisplayable buckets, and also the Layer Displayable buckets if desired. */ public void recreateBuckets(final boolean layer_buckets) { recreateBuckets(al_layers, layer_buckets); } /** Recreate the ZDisplayable buckets for {@code layer}, and also the {@link Layer} {@link Displayable} buckets if desired. * @param layer The {@link Layer} to recreate {@link ZDisplayable} buckets for. * @param layer_buckets Whether to also recreate the {@link Layer}-specific buckets for images and text labels. */ public void recreateBuckets(final Layer layer, final boolean layer_buckets) { LayerBucket lb = new LayerBucket(layer); synchronized (lbucks) { lbucks.put(layer, lb); } if (layer_buckets && null != layer.root) layer.recreateBuckets(); } /** Regenerate the quad-tree bucket system for the ZDisplayable instances that have data at each of the given layers, * and optionally regenerate the buckets as well for the 2D Displayable instances of that layer as well. */ public void recreateBuckets(final Collection<Layer> layers, final boolean layer_buckets) { final HashMap<Layer,LayerBucket> m = new HashMap<Layer,LayerBucket>(); try { Process.progressive(layers, new TaskFactory<Layer,Object>() { @Override public Object process(final Layer layer) { LayerBucket lb = new LayerBucket(layer); synchronized (m) { m.put(layer, lb); } if (layer_buckets && null != layer.root) layer.recreateBuckets(); return null; } }, Process.NUM_PROCESSORS -1); // works even when there is only 1 core, since it checks and fixes the '0' processors request } catch (Exception e) { IJError.print(e); } synchronized (lbucks) { lbucks.clear(); lbucks.putAll(m); } } /** Checks only buckets for ZDisplayable, not any related to any layer. */ public void checkBuckets() { synchronized (lbucks) { if (!lbucks.isEmpty()) return; } recreateBuckets(false); } /** Returns the minimal 2D bounding box for Displayables of class @param c in all layers. */ public Rectangle getMinimalBoundingBox(final Class<?> c) { Rectangle r = null; for (final Layer la : al_layers) { if (null == r) r = la.getMinimalBoundingBox(c); else { Rectangle box = la.getMinimalBoundingBox(c); // may be null if Layer is empty if (null != box) r.add(box); } } return r; } /** Time vs DoStep. Not all steps may be specific for a single Displayable. */ final private TreeMap<Long,DoStep> edit_history = new TreeMap<Long,DoStep>(); /** The step representing the current diff state. */ private long current_edit_time = 0; private DoStep current_edit_step = null; /** Displayable vs its own set of time vs DoStep, for quick access, for those edits that are specific of a Displayable. * It's necessary to set a ground, starting point for any Displayable whose data will be edited. */ final private Map<Displayable,TreeMap<Long,DoStep>> dedits = new HashMap<Displayable,TreeMap<Long,DoStep>>(); /** Time vs DoStep; as steps are removed from the end of edit_history, they are put here. */ final private TreeMap<Long,DoStep> redo = new TreeMap<Long,DoStep>(); /** Whether an initial step should be added or not. */ final boolean prepareStep(final Object ob) { synchronized (edit_history) { if (0 == edit_history.size() || redo.size() > 0) return true; // Check if the last added entry contains the exact same elements and data DoStep step = edit_history.get(edit_history.lastKey()); boolean b = step.isIdenticalTo(ob); //Utils.log2(b + " == prepareStep for " + ob); // If identical, don't prepare one! return !b; } } /** If last step is not a DoEdit "data" step for d, then call addDataEditStep(d). */ boolean addPreDataEditStep(final Displayable d) { if ( null == current_edit_step || (current_edit_step.getD() != d || !((DoEdit)current_edit_step).containsKey("data"))) { //Utils.log2("Adding pre-data edit step"); //return addDataEditStep(d); return addEditStep(new Displayable.DoEdit(d).init(d, new String[]{"data"})); } return false; } /** A new undo step for the "data" field of Displayable d. */ boolean addDataEditStep(final Displayable d) { //Utils.log2("Adding data edit step"); // Adds "data", which contains width,height,affinetransform,links, and the data (points, areas, etc.) return addDataEditStep(d, new String[]{"data"}); } /** A new undo step for any desired fields of Displayable d. */ boolean addDataEditStep(final Displayable d, final String[] fields) { return addEditStep(new Displayable.DoEdit(d).init(d, fields)); } /** A new undo step for the "data" field of all Displayable in the set. */ public boolean addDataEditStep(final Set<? extends Displayable> ds) { return addDataEditStep(ds, new String[]{"data"}); } boolean addDataEditStep(final Set<? extends Displayable> ds, final String[] fields) { final Displayable.DoEdits edits = new Displayable.DoEdits(ds); edits.init(fields); return addEditStep(edits); } /** Add an undo step for the transformations of all Displayable in the layer. */ public void addTransformStep(final Layer layer) { addTransformStep(layer.getDisplayables()); } public void addTransformStep(final List<Layer> layers) { final ArrayList<Displayable> all = new ArrayList<Displayable>(); for (final Layer la : layers) all.addAll(la.getDisplayables()); addTransformStep(all); } /** Add an undo step for the transformations of all Displayable in hs. */ public void addTransformStep(final Collection<? extends Displayable> col) { //Utils.log2("Added transform step for col"); addEditStep(new Displayable.DoTransforms().addAll(col)); } /** Add an undo step for the transformations of all Displayable in col, with data as well (for Patch, data includes the CoordinateTransform). */ public DoEdits addTransformStepWithData(final Collection<? extends Displayable> col) { if (col.isEmpty()) return null; final Set<? extends Displayable> hs = col instanceof Set<?> ? (Set<? extends Displayable>)col : new HashSet<Displayable>(col); final DoEdits step = new Displayable.DoEdits(hs).init(new String[]{"data", "at", "width", "height"}); addEditStep(step); return step; } /** Includes all ZDisplayable that paint at any of the given layers. */ public Collection<Displayable> addTransformStepWithDataForAll(final Collection<Layer> layers) { if (layers.isEmpty()) return Collections.emptyList(); final Set<Displayable> hs = new HashSet<Displayable>(); for (final Layer layer : layers) hs.addAll(layer.getDisplayables()); for (final ZDisplayable zd : al_zdispl) { for (final Layer layer : layers) { if (zd.paintsAt(layer)) { hs.add((Displayable)zd); break; } } } addTransformStepWithData(hs); return hs; } /** Add an undo step for the transformations of all Displayable in all layers. */ public void addTransformStep() { //Utils.log2("Added transform step for all"); Displayable.DoTransforms dt = new Displayable.DoTransforms(); for (final Layer la : al_layers) { dt.addAll(la.getDisplayables()); } addEditStep(dt); } /** Add a step to undo the addition or deletion of one or more objects in this project and LayerSet. */ public DoChangeTrees addChangeTreesStep() { DoChangeTrees step = new LayerSet.DoChangeTrees(this); if (prepareStep(step)) { Utils.log2("Added change trees step."); addEditStep(step); } return step; } /** Add a step to undo the addition or deletion of one or more objects in this project and LayerSet, * along with an arbitrary set of steps that may alter, for example the data. */ public DoChangeTrees addChangeTreesStep(final Set<DoStep> dependents) { DoChangeTrees step = addChangeTreesStep(); step.addDependents(dependents); addEditStep(step); return step; } /** For the Displayable contained in a Layer: their number, and their stack order. */ public void addLayerContentStep(final Layer la) { DoStep step = new Layer.DoContentChange(la); if (prepareStep(step)) { Utils.log2("Added layer content step."); addEditStep(step); } } /** For the Z and thickness of a layer. */ public void addLayerEditedStep(final Layer layer) { addEditStep(new Layer.DoEditLayer(layer)); } /** For the Z and thickness of a list of layers. */ public void addLayerEditedStep(final List<Layer> al) { addEditStep(new Layer.DoEditLayers(al)); } public void addUndoStep(final DoStep step) { addEditStep(step); } boolean addEditStep(final DoStep step) { if (null == step || step.isEmpty()) { Utils.log2("Warning: can't add empty step " + step); return false; } synchronized (edit_history) { // Check if it's identical to current step if (step.isIdenticalTo(current_edit_step)) { //Utils.log2("Skipping identical undo step of class " + step.getClass() + ": " + step); return false; } // Store current in undo queue if (null != current_edit_step) { edit_history.put(current_edit_time, current_edit_step); // Store for speedy access, if its Displayable-specific: final Displayable d = current_edit_step.getD(); if (null != d) { TreeMap<Long,DoStep> edits = dedits.get(d); if (null == edits) { edits = new TreeMap<Long,DoStep>(); dedits.put(d, edits); } edits.put(current_edit_time, current_edit_step); } // prune if too large while (edit_history.size() > project.getProperty("n_undo_steps", 32)) { long t = edit_history.firstKey(); DoStep st = edit_history.remove(t); if (null != st.getD()) { TreeMap<Long,DoStep> m = dedits.get(st.getD()); m.remove(t); if (0 == m.size()) dedits.remove(st.getD()); } } } // Set step as current current_edit_time = System.currentTimeMillis(); current_edit_step = step; // Bye bye redo! Can't branch. redo.clear(); } return true; } public boolean canUndo() { return edit_history.size() > 0; } public boolean canRedo() { return redo.size() > 0 || null != current_edit_step; } /** Undoes one step of the ongoing transformation history, otherwise of the overall LayerSet history. */ public boolean undoOneStep() { synchronized (edit_history) { if (0 == edit_history.size()) { Utils.logAll("Empty undo history!"); return false; } //Utils.log2("Undoing one step"); // Add current (if any) to redo queue if (null != current_edit_step) { redo.put(current_edit_time, current_edit_step); } // Remove last step from undo queue, and set it as current current_edit_time = edit_history.lastKey(); current_edit_step = edit_history.remove(current_edit_time); // Remove as well from dedits if (null != current_edit_step.getD()) { dedits.get(current_edit_step.getD()).remove(current_edit_time); } if (!current_edit_step.apply(DoStep.UNDO)) { Utils.log("Undo: could not apply step!"); return false; } Utils.log("Undoing " + current_edit_step.getClass().getSimpleName()); Display.updateVisibleTabs(project); } return true; } protected boolean removeLastUndoStep() { synchronized (edit_history) { if (edit_history.isEmpty()) return false; final long time = edit_history.lastKey(); final DoStep step = edit_history.remove(time); if (null != step.getD()) dedits.get(step.getD()).remove(time); // shift current if (step == current_edit_step) { current_edit_time = edit_history.lastKey(); current_edit_step = edit_history.get(current_edit_time); } } return true; } /** Redoes one step of the ongoing transformation history, otherwise of the overall LayerSet history. */ public boolean redoOneStep() { synchronized (edit_history) { if (0 == redo.size()) { Utils.logAll("Empty redo history!"); if (null != current_edit_step) { return current_edit_step.apply(DoStep.REDO); } return false; } //Utils.log2("Redoing one step"); // Add current (if any) to undo queue if (null != current_edit_step) { edit_history.put(current_edit_time, current_edit_step); if (null != current_edit_step.getD()) { dedits.get(current_edit_step.getD()).put(current_edit_time, current_edit_step); } } // Remove one step from undo queue and set it as current current_edit_time = redo.firstKey(); current_edit_step = redo.remove(current_edit_time); if (!current_edit_step.apply(DoStep.REDO)) { Utils.log("Undo: could not apply step!"); return false; } Utils.log("Redoing " + current_edit_step.getClass().getSimpleName()); Display.updateVisibleTabs(project); } return true; } static public void applyTransforms(final Map<Displayable,AffineTransform> m) { for (final Map.Entry<Displayable,AffineTransform> e : m.entrySet()) { e.getKey().setAffineTransform(e.getValue()); // updates buckets } } static { // Undo background tasks: should be done in background threads, // but on attempting to undo/redo, the undo/redo should wait // until all tasks are done. For example, updating mipmaps when // undoing/redoing min/max or CoordinateTransform. // This could be done with futures: spawn and do in the // background, but on redo/undo, call for the Future return // value, which will block until there is one to return. // Since blocking would block the EventDispatchThread, just refuse to undo/redo and notify the user. // // TODO } /** Keeps the width,height of a LayerSet and the AffineTransform of every Displayable in it. */ static private class DoResizeLayerSet implements DoStep { final LayerSet ls; final HashMap<Displayable,AffineTransform> affines; final float width, height; DoResizeLayerSet(final LayerSet ls) { this.ls = ls; this.width = ls.layer_width; this.height = ls.layer_height; this.affines = new HashMap<Displayable,AffineTransform>(); final ArrayList<Displayable> col = ls.getDisplayables(); // it's a new list col.addAll(ls.getZDisplayables()); for (final Displayable d : col) { this.affines.put(d, d.getAffineTransformCopy()); } } public boolean isIdenticalTo(final Object ob) { if (!(ob instanceof LayerSet)) return false; final LayerSet layerset = (LayerSet) ob; if (layerset.layer_width != this.width || layerset.height != this.height || layerset != this.ls) return false; final ArrayList<Displayable> col = ls.getDisplayables(); col.addAll(ls.getZDisplayables()); for (final Displayable d : col) { final AffineTransform aff = this.affines.get(d); if (null == aff) return false; if (!aff.equals(d.getAffineTransform())) return false; } return true; } public boolean apply(int action) { ls.layer_width = width; ls.layer_height = height; for (final Map.Entry<Displayable,AffineTransform> e : affines.entrySet()) { e.getKey().getAffineTransform().setTransform(e.getValue()); } final boolean dobuckets; synchronized (ls.lbucks) { dobuckets = ls.lbucks.isEmpty(); } if (dobuckets) ls.recreateBuckets(true); Display.updateSelection(); Display.update(ls); //so it's not left out painted beyond borders return true; } public boolean isEmpty() { return false; } public Displayable getD() { return null; } } /** Records the state of the LayerSet.al_layers, each Layer.al_displayables and all the trees and unique types of Project. */ static protected class DoChangeTrees implements DoStep { final LayerSet ls; final HashMap<Thing,Boolean> ttree_exp, ptree_exp, ltree_exp; final Thing troot, proot, lroot; final ArrayList<Layer> all_layers; final HashMap<Layer,ArrayList<Displayable>> all_displ; final ArrayList<ZDisplayable> all_zdispl; final HashMap<Displayable,Set<Displayable>> links; final HashMap<Long,Layer> idlayers; final HashMap<Layer,Integer> layerindices; HashSet<DoStep> dependents = null; // TODO: does not consider recursive LayerSets! public DoChangeTrees(final LayerSet ls) { this.ls = ls; final Project p = ls.getProject(); this.ttree_exp = new HashMap<Thing,Boolean>(); this.troot = p.getTemplateTree().duplicate(this.ttree_exp); this.ptree_exp = new HashMap<Thing,Boolean>(); this.proot = p.getProjectTree().duplicate(this.ptree_exp); this.ltree_exp = new HashMap<Thing,Boolean>(); this.lroot = p.getLayerTree().duplicate(this.ltree_exp); this.all_layers = ls.getLayers(); // a copy of the list, but each object is the running instance this.all_zdispl = ls.getZDisplayables(); // idem this.idlayers = new HashMap<Long,Layer>(ls.idlayers); synchronized (ls.layerindices) { this.layerindices = new HashMap<Layer,Integer>(ls.layerindices); } this.links = new HashMap<Displayable,Set<Displayable>>(); for (final ZDisplayable zd : this.all_zdispl) { this.links.put(zd, zd.hs_linked); // LayerSet is a Displayable } this.all_displ = new HashMap<Layer,ArrayList<Displayable>>(); for (final Layer layer : all_layers) { final ArrayList<Displayable> al = layer.getDisplayables(); // a copy this.all_displ.put(layer, al); for (final Displayable d : al) { this.links.put(d, null == d.hs_linked ? null : new HashSet<Displayable>(d.hs_linked)); } } } public Displayable getD() { return null; } public boolean isEmpty() { return false; } public boolean isIdenticalTo(final Object ob) { // TODO return false; } public boolean apply(int action) { // Replace all trees final Project p = ls.getProject(); p.resetRootTemplateThing((TemplateThing)this.troot, ttree_exp); p.resetRootProjectThing((ProjectThing)this.proot, ptree_exp); p.resetRootLayerThing((LayerThing)this.lroot, ltree_exp); // Replace all layers ls.al_layers.clear(); ls.al_layers.addAll(this.all_layers); synchronized (ls.IDLAYERS_WRITE_LOCK) { ls.idlayers = new HashMap<Long,Layer>(this.idlayers); } synchronized (ls.layerindices) { ls.layerindices.clear(); ls.layerindices.putAll(this.layerindices); } // Replace all Displayable in each Layer for (final Map.Entry<Layer,ArrayList<Displayable>> e : all_displ.entrySet()) { // Acquire pointer to the actual instance list in each Layer final ArrayList<Displayable> al = e.getKey().getDisplayableList(); // the real one! // Create a list to contain those Displayable present in old list but not in list to use now final HashSet<Displayable> diff = new HashSet<Displayable>(al); // create with all Displayable of old list diff.removeAll(e.getValue()); // remove all Displayable present in list to use now, to leave the diff or remainder only // Clear current list al.clear(); // Insert all to the current list al.addAll(e.getValue()); // Add to remove-on-shutdown queue all those Patch no longer in the list to use now: for (final Displayable d : diff) { if (d.getClass() == Patch.class) { d.getProject().getLoader().tagForMipmapRemoval((Patch)d, true); } } // Remove from queue all those Patch in the list to use now: for (final Displayable d : al) { if (d.getClass() == Patch.class) { d.getProject().getLoader().tagForMipmapRemoval((Patch)d, false); } } } // Replace all ZDisplayable ls.al_zdispl.clear(); ls.al_zdispl.addAll(this.all_zdispl); // Replace all links for (final Map.Entry<Displayable,Set<Displayable>> e : this.links.entrySet()) { final Set<Displayable> hs = e.getKey().hs_linked; if (null != hs) { final Set<Displayable> hs2 = e.getValue(); if (null == hs2) e.getKey().hs_linked = null; else { hs.clear(); hs.addAll(hs2); } } } // Invoke dependents if (null != dependents) for (DoStep step : dependents) step.apply(action); ls.recreateBuckets(true); Display.clearSelection(ls.project); Display.update(ls, false); return true; } synchronized public void addDependents(Set<DoStep> dep) { if (null == this.dependents) this.dependents = new HashSet<DoStep>(); this.dependents.addAll(dep); } } /** To undo moving up/down/top/bottom. */ public DoStep createUndoMoveStep(final Displayable d) { return d instanceof ZDisplayable ? new LayerSet.DoMoveZDisplayable(this) : new Layer.DoMoveDisplayable(d.getLayer()); } /** To undo moving up/down/top/bottom. */ public void addUndoMoveStep(final Displayable d) { addUndoStep(createUndoMoveStep(d)); } static protected class DoMoveZDisplayable implements DoStep { final ArrayList<ZDisplayable> al_zdispl; final LayerSet ls; HashSet<DoStep> dependents = null; DoMoveZDisplayable(final LayerSet ls) { this.ls = ls; this.al_zdispl = new ArrayList<ZDisplayable>(ls.al_zdispl); } @Override public boolean apply(int action) { // Replace all ZDisplayable ls.al_zdispl.clear(); ls.al_zdispl.addAll(this.al_zdispl); Display.update(ls, false); return true; } @Override public boolean isEmpty() { return false; } @Override public Displayable getD() { return null; } @Override public boolean isIdenticalTo(Object ob) { if (!(ob instanceof DoMoveZDisplayable)) return false; final DoMoveZDisplayable dmz = (DoMoveZDisplayable)ob; if (dmz.ls != this.ls) return false; if (dmz.al_zdispl.size() != this.al_zdispl.size()) return false; for (int i=0; i<this.al_zdispl.size(); ++i) { if (dmz.al_zdispl.get(i) != this.al_zdispl.get(i)) return false; } return true; } } private Overlay overlay = null; /** Return the current Overlay or a new one if none yet. */ synchronized public Overlay getOverlay() { if (null == overlay) overlay = new Overlay(); return overlay; } // Used by DisplayCanvas to paint Overlay getOverlay2() { return overlay; } /** Set to null to remove the Overlay. * @return the previous Overlay, if any. */ synchronized public Overlay setOverlay(final Overlay o) { Overlay old = this.overlay; this.overlay = o; return old; } private final HashMap<DisplayCanvas.ScreenshotProperties,DisplayCanvas.Screenshot> offscreens = new HashMap<DisplayCanvas.ScreenshotProperties,DisplayCanvas.Screenshot>(); private final HashMap<Layer,HashSet<DisplayCanvas.Screenshot>> offscreens2 = new HashMap<Layer,HashSet<DisplayCanvas.Screenshot>>(); final DisplayCanvas.Screenshot getScreenshot(final DisplayCanvas.ScreenshotProperties props) { synchronized (offscreens) { return offscreens.get(props); } } final private void putO2(final Layer la, final DisplayCanvas.Screenshot sc) { HashSet<DisplayCanvas.Screenshot> hs = offscreens2.get(la); if (null == hs) { hs = new HashSet<DisplayCanvas.Screenshot>(); offscreens2.put(la, hs); } hs.add(sc); } final private void removeO2(final DisplayCanvas.Screenshot sc) { HashSet<DisplayCanvas.Screenshot> hs = offscreens2.get(sc.layer); if (null == hs) return; hs.remove(sc); } final void storeScreenshot(DisplayCanvas.Screenshot s) { synchronized(offscreens) { offscreens.put(s.props, s); putO2(s.layer, s); } } final void clearScreenshots() { synchronized (offscreens) { for (final DisplayCanvas.Screenshot s : offscreens.values()) { s.flush(); } offscreens.clear(); offscreens2.clear(); } } final void trimScreenshots() { synchronized(offscreens) { if (offscreens.size() > 1000) { TreeMap<Long,DisplayCanvas.Screenshot> m = new TreeMap<Long,DisplayCanvas.Screenshot>(); for (final DisplayCanvas.Screenshot s : offscreens.values()) { m.put(s.born, s); } offscreens.clear(); offscreens2.clear(); ArrayList<Long> t = new ArrayList<Long>(m.keySet()); for (final DisplayCanvas.Screenshot sc : m.subMap(m.firstKey(), t.get(t.size()/2)).values()) { offscreens.put(sc.props, sc); putO2(sc.layer, sc); } // not flushing: they will get thrown out eventually } } } final void removeFromOffscreens(final DisplayCanvas.Screenshot sc) { synchronized (offscreens) { offscreens.remove(sc.props); removeO2(sc); } } public final void removeFromOffscreens(final Layer la) { synchronized (offscreens) { final HashSet<DisplayCanvas.Screenshot> hs = offscreens2.remove(la); if (null != hs) { for (final DisplayCanvas.Screenshot sc : hs) { offscreens.remove(sc.props); } } } } final void removeFromOffscreens(final ZDisplayable zd) { synchronized (offscreens) { // Throw away any cached that intersect the zd final Rectangle box = zd.getBoundingBox(); for (final Iterator<DisplayCanvas.Screenshot> it = offscreens.values().iterator(); it.hasNext(); ) { final DisplayCanvas.Screenshot sc = it.next(); if (box.intersects(sc.props.srcRect)) { it.remove(); final HashSet<DisplayCanvas.Screenshot> hs = offscreens2.get(sc.layer); if (null != hs) hs.remove(sc.props); } } } } final boolean containsScreenshot(final DisplayCanvas.Screenshot sc) { synchronized (offscreens) { return offscreens.containsKey(sc.props); } } /** Find all java.awt.geom.Area in layer that intersect with box, if visible. * Areas are returned as they are, with coords local to the Displayable they come from. * Modifying the Area instances will modify the actual data in the AreaContainer Displayable. */ protected Map<Displayable,List<Area>> findAreas(final Layer layer, final Rectangle box, final boolean visible) { final Map<Displayable,List<Area>> m = new HashMap<Displayable,List<Area>>(); for (final Displayable zd : findZDisplayables(layer, box, visible)) { if (!(zd instanceof AreaContainer)) continue; List<Area> a = ((AreaContainer)zd).getAreas(layer, box); if (null == a) continue; m.put(zd, a); } return m; } /** A set of unique tags, retrievable by their own identity. */ protected final Map<Integer,HashMap<String,Tag>> tags = new HashMap<Integer,HashMap<String,Tag>>(); { final Tag TODO = new Tag("TODO", KeyEvent.VK_T), UNCERTAIN_END = new Tag("Uncertain end", KeyEvent.VK_U); final HashMap<String,Tag> m1 = new HashMap<String,Tag>(), m2 = new HashMap<String,Tag>(); m1.put(TODO.toString(), TODO); m2.put(UNCERTAIN_END.toString(), UNCERTAIN_END); tags.put(KeyEvent.VK_T, m1); tags.put(KeyEvent.VK_U, m2); } /** Returns an existing immutable Tag instance if already there, or stores a new one and returns it. */ public Tag putTag(final String tag, final int keyCode) { if (null == tag) return null; synchronized (tags) { HashMap<String,Tag> ts = tags.get(keyCode); if (null == ts) { ts = new HashMap<String,Tag>(); tags.put(keyCode, ts); } final Tag t = new Tag(tag, keyCode); final Tag existing = ts.get(t); if (null == existing) { ts.put(t.toString(), t); return t; } else { return existing; } } } /** If there aren't any tags for keyCode, returns an empty TreeSet. */ @SuppressWarnings("unchecked") public TreeSet<Tag> getTags(final int keyCode) { synchronized (tags) { final HashMap<String,Tag> ts = tags.get(keyCode); return new TreeSet<Tag>(null == ts ? Collections.EMPTY_SET : KeyEvent.VK_R == keyCode ? filterReviewTags(ts.values()) : ts.values()); } } private final Collection<Tag> filterReviewTags(final Collection<Tag> ts) { final ArrayList<Tag> a = new ArrayList<Tag>(); for (final Tag tag : ts) { if ('#' == tag.toString().charAt(0)) continue; else a.add(tag); } return a; } protected Tag askForNewTag(final int keyCode) { GenericDialog gd = new GenericDialog("Define new tag"); gd.addMessage("Define new tag for key: " + ((char)keyCode)); TreeSet<Tag> ts = getTags(keyCode); gd.addStringField("New tag:", "", 40); if (null != ts && ts.size() > 0) { String[] names = new String[ts.size()]; int next = 0; for (Tag t : ts) names[next++] = t.toString(); gd.addChoice("Existing tags for " + ((char)keyCode) + ":", names, names[0]); } gd.showDialog(); if (gd.wasCanceled()) return null; String tag = gd.getNextString().trim(); if (0 == tag.length()) { Utils.logAll("Invalid tag " + tag); return null; } return putTag(tag, keyCode); } /** Returns false if the dialog was canceled or there wasn't any tag to remove. */ protected boolean askToRemoveTag(final int keyCode) { TreeSet<Tag> ts = getTags(keyCode); if (null == ts || ts.isEmpty()) return false; String[] tags = new String[ts.size()]; int next = 0; for (Tag t : ts) tags[next++] = t.toString(); GenericDialog gd = new GenericDialog("Remove tag"); gd.addMessage("Remove a tag for key: " + ((char)keyCode)); gd.addChoice("Remove:", tags, tags[0]); gd.showDialog(); if (gd.wasCanceled()) return false; String tag = gd.getNextChoice(); removeTag(tag, keyCode); return true; } /** Removes the tag from the list of possible tags, and then from wherever it has been assigned. The @param tag is duck-typed. */ public void removeTag(final String tag, final int keyCode) { removeTag(new Tag(tag, keyCode)); } public void removeTag(final Tag t) { synchronized (tags) { HashMap<String,Tag> ts = tags.get(t.getKeyCode()); if (null == ts) return; ts.remove(t.toString()); } for (final Displayable d : getDisplayables()) { d.removeTag(t); } for (final ZDisplayable zd : al_zdispl) { zd.removeTag(t); } Display.repaint(this); } public void removeAllTags() { // the easy and unperformant way ... I have better things to do for (final HashMap<String,Tag> m : tags.values()) { for (final Tag t : m.values()) { removeTag(t); } } } public String exportTags() { StringBuilder sb = new StringBuilder("<tags>\n"); for (final Map.Entry<Integer,HashMap<String,Tag>> e : tags.entrySet()) { final char key = (char)e.getKey().intValue(); for (final Tag t : e.getValue().values()) { sb.append(" <tag key=\"").append(key).append("\" val=\"").append(t.toString()).append("\" />\n"); } } return sb.append("</tags>").toString(); } public void importTags(String path, boolean replace) { HashMap<Integer,HashMap<String,Tag>> backup = new HashMap<Integer,HashMap<String,Tag>>(this.tags); // copy! InputStream istream = null; try { if (replace) removeAllTags(); SAXParserFactory f = SAXParserFactory.newInstance(); f.setValidating(false); SAXParser parser = f.newSAXParser(); istream = Utils.createStream(path); parser.parse(new InputSource(istream), new TagsParser()); } catch (Throwable t) { IJError.print(t); // restore: this.tags.clear(); this.tags.putAll(backup); // no undo for all potentially removed tags ... } finally { try { if (null != istream) istream.close(); } catch (Exception e) {} } } private class TagsParser extends DefaultHandler { public void startElement(String uri, String localName, String qName, Attributes attributes) { if (!"tag".equals(qName.toLowerCase())) return; final HashMap<String,String> m = new HashMap<String,String>(); for (int i=attributes.getLength() -1; i>-1; i--) { m.put(attributes.getQName(i).toLowerCase(), attributes.getValue(i)); } final String key = m.get("key"), content = m.get("val"); if (null == key || key.length() > 1 || Character.isDigit(key.charAt(0)) || null == content) { Utils.log("Ignoring invalid tag with key '" + key + "' and value '" + content + "'"); return; } putTag(content, (int)key.charAt(0)); } } // ==== GET ZDisplayable objects ==== // // ESSENTIALLY, a filter operation on the al_zdispl list. public ArrayList<ZDisplayable> getZDisplayables(final Class<?> c) { return getZDisplayables(c, false); } /** Returns a list of ZDisplayable of class c only. * If @param instance_of, use c.isInstance(...) instead of class equality. */ public ArrayList<ZDisplayable> getZDisplayables(final Class<?> c, final boolean instance_of) { final ArrayList<ZDisplayable> al = new ArrayList<ZDisplayable>(); if (null == c) return al; if (Displayable.class == c || ZDisplayable.class == c) { al.addAll(al_zdispl); return al; } if (instance_of) { for (final ZDisplayable zd : al_zdispl) { if (c.isInstance(zd)) al.add(zd); } } else { for (final ZDisplayable zd : al_zdispl) { if (zd.getClass() == c) al.add(zd); } } return al; } // FILTER operations but also by an Area in a given Layer: /** Use method findZDisplayables(...) instead. */ @Deprecated public ArrayList<ZDisplayable> getZDisplayables(final Class<?> c, final Layer layer, final Area aroi, final boolean visible_only) { return getZDisplayables(c, layer, aroi, visible_only, false); } /** Use method findZDisplayables(...) instead. */ @SuppressWarnings({ "unchecked", "rawtypes" }) @Deprecated public ArrayList<ZDisplayable> getZDisplayables(final Class<?> c, final Layer layer, final Area aroi, final boolean visible_only, final boolean instance_of) { if (!ZDisplayable.class.isAssignableFrom(c)) return new ArrayList<ZDisplayable>(); return new ArrayList<ZDisplayable>((Collection<ZDisplayable>)(Collection)findZDisplayables(c, layer, aroi, visible_only, instance_of)); } // ============== FIND Displayable or ZDisplayable onjects ============ /** Find any Displayable or ZDisplayable objects of class C which intersect with the Area @param aroi. If @param visible_only, then only those that are not hidden. */ public ArrayList<Displayable> find(final Class<?> c, final Layer layer, final Area aroi, final boolean visible_only) { return find(c, layer, aroi, visible_only, false); } /** Find any Displayable or ZDisplayable objects of class C which intersect with the Area @param aroi. If @param visible_only, then only those that are not hidden. If @param instance_of is true, then classes are not check by equality but by instanceof. */ public ArrayList<Displayable> find(final Class<?> c, final Layer layer, final Area aroi, final boolean visible_only, final boolean instance_of) { final ArrayList<Displayable> al = new ArrayList<Displayable>(); if (!ZDisplayable.class.isAssignableFrom(c)) { al.addAll(layer.getDisplayables(c, aroi, visible_only, instance_of)); } al.addAll(findZDisplayables(c, layer, aroi, visible_only, instance_of)); return al; } public ArrayList<Displayable> find(final Class<?> c, final Layer layer, final int x, final int y, final boolean visible_only) { final ArrayList<Displayable> al = new ArrayList<Displayable>(); if (!ZDisplayable.class.isAssignableFrom(c)) { al.addAll(layer.find(c, x, y, visible_only)); } al.addAll(findZDisplayables(c, layer, x, y, visible_only)); return al; } // ======== FIND ZDisplayable only ======= /** Find ZDisplayable objects that contain the point x,y in the given layer. */ public Collection<Displayable> findZDisplayables(final Layer layer, final int x, final int y, final boolean visible_only) { final LayerBucket lb; synchronized (lbucks) { lb = lbucks.get(layer); } if (null != lb) return lb.root.find(x, y, layer, visible_only); else nbmsg(layer); final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (final ZDisplayable zd : al_zdispl) { if (zd.contains(layer, x, y)) al.add(zd); } return al; } /** Find ZDisplayable objects of Class c that contain the point x,y in the given layer. */ public Collection<Displayable> findZDisplayables(final Class<?> c, final Layer layer, final int x, final int y, final boolean visible_only) { return findZDisplayables(c, layer, x, y, visible_only, false); } /** Find ZDisplayable objects of Class c that contain the point x,y in the given layer. */ public Collection<Displayable> findZDisplayables(final Class<?> c, final Layer layer, final int x, final int y, final boolean visible_only, final boolean instance_of) { final LayerBucket lb; synchronized (lbucks) { lb = lbucks.get(layer); } if (null != lb) return lb.root.find(c, x, y, layer, visible_only, instance_of); else nbmsg(layer); final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (final ZDisplayable zd : al_zdispl) { if (zd.getClass() == c && zd.contains(layer, x, y)) al.add(zd); } return al; } public Collection<Displayable> findZDisplayables(final Class<?> c, final Layer layer, final Rectangle r, final boolean visible_only) { return findZDisplayables(c, layer, r, visible_only, false); } /** Find ZDisplayable objects of the given class that intersect the given rectangle in the given layer. */ public Collection<Displayable> findZDisplayables(final Class<?> c, final Layer layer, final Rectangle r, final boolean visible_only, final boolean instance_of) { final LayerBucket lb; synchronized (lbucks) { lb = lbucks.get(layer); } if (null != lb) return lb.root.find(c, r, layer, visible_only, instance_of); else nbmsg(layer); final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (final ZDisplayable zd : al_zdispl) { if (instance_of && !c.isInstance(zd)) continue; else if (zd.getClass() != c) continue; if (zd.getBounds(null, layer).intersects(r)) al.add(zd); } return al; } /** Find ZDisplayable objects of the given class that intersect the given area in the given layer. * If @param instance_of is true, use c.isAssignableFrom instead of class equality. */ public Collection<Displayable> findZDisplayables(final Class<?> c, final Layer layer, final Area aroi, final boolean visible_only, final boolean instance_of) { final LayerBucket lb; synchronized (lbucks) { lb = lbucks.get(layer); } if (null != lb) return lb.root.find(c, aroi, layer, visible_only, instance_of); else nbmsg(layer); final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (final ZDisplayable zd : al_zdispl) { if (visible_only && !zd.isVisible()) continue; if (instance_of) { if (!c.isAssignableFrom(zd.getClass())) continue; } else if (zd.getClass() != c) continue; if (zd.intersects(layer, aroi)) al.add(zd); } return al; } /** Find ZDisplayable objects that intersect the given rectangle in the given layer. */ public Collection<Displayable> findZDisplayables(final Layer layer, final Rectangle r, final boolean visible_only) { final LayerBucket lb; synchronized (lbucks) { lb = lbucks.get(layer); } if (null != lb) return lb.root.find(r, layer, visible_only); else nbmsg(layer); final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (final ZDisplayable zd : al_zdispl) { if (visible_only && !zd.isVisible()) continue; if (zd.getBounds(null, layer).intersects(r)) al.add(zd); } return al; } /** Find ZDisplayable objects that intersect the given rectangle in the given layer. * May return false positives but never false negatives. */ public Collection<Displayable> roughlyFindZDisplayables(final Layer layer, final Rectangle r, final boolean visible_only) { final LayerBucket lb; synchronized (lbucks) { lb = lbucks.get(layer); } if (null != lb) return lb.root.roughlyFind(r, layer, visible_only); else nbmsg(layer); // Else, linear: final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (final ZDisplayable zd : al_zdispl) { if (visible_only && !zd.isVisible()) continue; if (zd.getBounds(null, layer).intersects(r)) al.add(zd); } return al; } private static final void nbmsg(final Layer la) { Utils.log2("No buckets for layer " + la); } /** Get all Displayable or ZDisplayable of the given class. * Classes are tested by equality, except for ZDisplayable.class. * Will also consider Displayable.class and subclasses in * a similar fashion, by calling Layer.getAll(c). */ @SuppressWarnings("unchecked") public<T extends Displayable> List<T> getAll(final Class<T> c) { final ArrayList<T> al = new ArrayList<T>(); if (null == c) return al; if (ZDisplayable.class == c) { al.addAll((Collection<T>)al_zdispl); } else if (ZDisplayable.class.isAssignableFrom(c)) { for (final ZDisplayable d : al_zdispl) { if (d.getClass() == c) al.add((T)d); } } else if (Displayable.class == c) { for (final Layer la : al_layers) { al.addAll((Collection<T>)la.getDisplayables()); } al.addAll((Collection<T>)al_zdispl); } else if (Displayable.class.isAssignableFrom(c)) { for (final Layer la : al_layers) { al.addAll(la.getAll(c)); } } return al; } }