/**
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.gui.GenericDialog;
import ini.trakem2.ControlWindow;
import ini.trakem2.Project;
import ini.trakem2.persistence.DBObject;
import ini.trakem2.persistence.XMLOptions;
import ini.trakem2.tree.LayerThing;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.M;
import ini.trakem2.utils.Utils;
import java.awt.Color;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.NoninvertibleTransformException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import mpicbg.models.NoninvertibleModelException;
public final class Layer extends DBObject implements Bucketable, Comparable<Layer> {
private final ArrayList<Displayable> al_displayables = new ArrayList<Displayable>();
/** For fast search. */
Bucket root = null;
private HashMap<Displayable,HashSet<Bucket>> db_map = null;
private double z = 0;
private double thickness = 0;
private LayerSet parent;
/** Compare layers by Z. */
static public final Comparator<Layer> COMPARATOR = new Comparator<Layer>() {
@Override
public final int compare(final Layer l1, final Layer l2) {
if (l1 == l2) return 0; // the same layer
if (l1.getZ() < l2.getZ()) return -1;
return 1; // even if same Z, prefer the second
}
@Override
public final boolean equals(final Object ob) { return this == ob; }
};
public Layer(final Project project, final double z, final double thickness, final LayerSet parent) {
super(project);
this.z = z;
this.thickness = thickness;
this.parent = parent;
addToDatabase();
}
/** Reconstruct from database*/
public Layer(final Project project, final long id, final double z, final double thickness) {
super(project, id);
this.z = z;
this.thickness = thickness;
this.parent = null;
}
/** Reconstruct from XML file. */
public Layer(final Project project, final long id, final HashMap<String,String> ht_attributes) {
super(project, id);
this.parent = null;
// parse data
String data;
if (null != (data = ht_attributes.get("z"))) this.z = Double.parseDouble(data);
else Displayable.xmlError(this, "z", this.z);
if (null != (data = ht_attributes.get("thickness"))) this.thickness = Double.parseDouble(data);
else Displayable.xmlError(this, "thickness", this.thickness);
}
/** Creates a new Layer asking for z and thickness, and adds it to the parent and returns it. Returns null if the dialog was canceled.*/
static public Layer create(final Project project, final LayerSet parent) {
if (null == parent) return null;
final GenericDialog gd = ControlWindow.makeGenericDialog("New Layer");
gd.addMessage("In pixels:"); // TODO set elsewhere the units!
gd.addNumericField("z coordinate: ", 0.0D, 3);
gd.addNumericField("thickness: ", 1.0D, 3);
gd.showDialog();
if (gd.wasCanceled()) return null;
try {
final double z = gd.getNextNumber();
final double thickness = gd.getNextNumber();
if (Double.isNaN(z) || Double.isNaN(thickness)) return null;
final Layer layer = new Layer(project, z, thickness, parent);
parent.add(layer);
parent.recreateBuckets(layer, true);
return layer;
} catch (final Exception e) {}
return null;
}
/** Pops up a dialog to choose the first Z coord, the thickness, the number of layers,
* and whether to skip the creation of any layers whose Z and thickness match
* that of existing layers.
* @return The newly created layers. */
static public List<Layer> createMany(final Project project, final LayerSet parent) {
if (null == parent) return null;
final GenericDialog gd = ControlWindow.makeGenericDialog("Many new layers");
gd.addNumericField("First Z coord: ", 0, 3);
gd.addNumericField("thickness: ", 1.0, 3);
gd.addNumericField("Number of layers: ", 1, 0);
gd.addCheckbox("Skip existing layers", true);
gd.showDialog();
if (gd.wasCanceled()) return null;
// start iteration to add layers
double z = gd.getNextNumber();
final double thickness = gd.getNextNumber();
final int n_layers = (int)gd.getNextNumber();
final boolean skip = gd.getNextBoolean();
if (thickness < 0) {
Utils.log("Can't create layers with negative thickness");
return null;
}
if (n_layers < 1) {
Utils.log("Invalid number of layers");
return null;
}
final List<Layer> layers = new ArrayList<Layer>(n_layers);
for (int i=0; i<n_layers; i++) {
Layer la = null;
if (skip) {
// Check if layer exists
la = parent.getLayer(z);
if (null == la) la = new Layer(project, z, thickness, parent);
else la = null;
} else la = new Layer(project, z, thickness, parent);
if (null != la) {
parent.addSilently(la);
layers.add(la);
}
z += thickness;
}
parent.recreateBuckets(layers, true); // all empty
// update the scroller of currently open Displays
Display.updateLayerScroller(parent);
return layers;
}
/** Returns a title such as 018__4-K4_2__z1.67 , which is [layer_set index]__[title]__[z coord] . The ordinal number starts at 1 and finishes at parent's length, inclusive. */
public String getPrintableTitle() {
final LayerThing lt = project.findLayerThing(this);
if (null == lt) return toString();
String title = lt.getTitle();
if (null == title) title = "";
else title = title.replace(' ', '_');
final StringBuilder sb = new StringBuilder().append(parent.indexOf(this) + 1);
final int s_size = Integer.toString(parent.size()).length();
while (sb.length() < s_size) {
sb.insert(0, '0');
}
sb.append('_').append('_').append(title).append('_').append('_').append('z').append(Utils.cutNumber(this.z, 3, true));
return sb.toString();
}
@Override
public String toString() {
if (null == parent) return new StringBuilder("z=").append(Utils.cutNumber(z, 4)).toString();
//return "z=" + Utils.cutNumber(z / parent.getCalibration().pixelDepth * z !!!?? I don't have the actual depth to correct with.
//return "z=" + Utils.cutNumber(z, 4);
final String unit = parent.getCalibration().getUnit();
if (unit.equals("pixel")) {
return "z=" + Utils.cutNumber(z, 4);
}
return "z=" + (z * parent.getCalibration().pixelWidth) + " " + unit
+ " (" + Utils.cutNumber(z, 4) + " px)";
}
/** Add a displayable and update all Display instances showing this Layer. */
public void add(final Displayable displ) { add(displ, true); }
public void add(final Displayable displ, final boolean update_displays) {
add(displ, update_displays, true);
}
public void add(final Displayable displ, final boolean update_displays, final boolean update_db) {
if (null == displ || -1 != al_displayables.indexOf(displ)) return;
if (displ.getProject() != this.project)
throw new IllegalArgumentException("Layer rejected a Displayable: belongs to a different project.");
int i=-1, j=-1;
final Displayable[] d = new Displayable[al_displayables.size()];
al_displayables.toArray(d);
int stack_index = 0;
// what is it?
if (displ instanceof Patch) {
// find last Patch (which start at 0)
for (i=0; i<d.length; i++) {
if (d[i] instanceof Patch) { j = i;}
else break;
}
if (-1 != j) {
j++;
if (j >= d.length) {
al_displayables.add(displ); // at the end
stack_index = d.length;
} else {
al_displayables.add(j, displ);
stack_index = j;
}
} else {
// no patches
al_displayables.add(0, displ); // at the very beggining
stack_index = 0;
}
} else if (displ instanceof Profile) {
// find first LayerSet or if none, first DLabel, add before it
for (i=d.length-1; i>-1; i--) {
if (! (d[i] instanceof DLabel || d[i] instanceof LayerSet)) { j = i; break; }
}
if (-1 != j) {
j++;
if (j >= d.length) { al_displayables.add(displ); stack_index = d.length; }
else { al_displayables.add(j, displ); stack_index = j; }
} else {
// no labels or LayerSets
al_displayables.add(displ); // at the end
stack_index = d.length;
}
} else if (displ instanceof LayerSet) {
// find first DLabel, add before it
for (i=d.length-1; i>-1; i--) {
if (! (d[i] instanceof DLabel)) { j = i; break; }
}
if (-1 != j) {
j++; // add it after the non-label one, displacing the label one position
if (j >= d.length) { al_displayables.add(displ); stack_index = d.length; } // at the end
else { al_displayables.add(j, displ); stack_index = j; }
} else {
// no labels
al_displayables.add(displ); // at the end
stack_index = d.length;
}
} else {
// displ is a DLabel
al_displayables.add(displ); // at the end
stack_index = d.length;
}
if (update_db) {
updateInDatabase("stack_index"); // of the displayables ...
displ.setLayer(this);
} else {
displ.setLayer(this, false);
}
// insert into bucket
if (null != root) {
if (d.length == stack_index) {
// append at the end
root.put(stack_index, displ, this, db_map);
} else {
// add as last first, then update
root.put(d.length, displ, this, db_map);
// find and update the range of affected Displayable objects
root.updateRange(this, displ, stack_index, d.length); // first to last indices affected
}
}
if (update_displays) {
Display.add(this, displ);
}
}
@Override
public HashMap<Displayable, HashSet<Bucket>> getBucketMap(final Layer layer) { // ignore layer
return db_map;
}
/** Used for reconstruction purposes. Assumes the displ are given in the proper order! */
public void addSilently(final DBObject displ) { // why DBObject and not Displayable ?? TODO
if (null == displ || -1 != al_displayables.indexOf(displ)) return;
try {
((Displayable)displ).setLayer(this, false);
al_displayables.add((Displayable)displ);
} catch (final Exception e) {
Utils.log("Layer.addSilently: Not a Displayable/LayerSet, not adding DBObject id=" + displ.getId());
return;
}
}
/** Will recreate the buckets; if you intend to remove many, use "removeAll" instead,
* so that the expensive operation of recreating the buckets is done only once. */
public synchronized boolean remove(final Displayable displ) {
if (null == displ || null == al_displayables) {
Utils.log2("Layer can't remove Displayable " + displ.getId());
return false;
}
final int old_stack_index = al_displayables.indexOf(displ);
if (-1 == old_stack_index) {
Utils.log2("Layer.remove: not found: " + displ);
return false;
}
al_displayables.remove(old_stack_index);
if (null != root) recreateBuckets();
parent.removeFromOffscreens(this);
Display.remove(this, displ);
return true;
}
/** Remove a set of children. Does not destroy the children nor remove them from the database, only from the Layer and the Display. */
public synchronized boolean removeAll(final Set<Displayable> ds) {
if (null == ds || null == al_displayables) return false;
// Ensure list is iterated only once: don't ask for index every time!
for (final Iterator<Displayable> it = al_displayables.iterator(); it.hasNext(); ) {
final Displayable d = it.next();
if (ds.contains(d)) {
it.remove();
parent.removeFromOffscreens(this);
Display.remove(this, d);
}
}
if (null != root) recreateBuckets();
Display.updateVisibleTabs(this.project);
return true;
}
/** Used for reconstruction purposes. */
public void setParentSilently(final LayerSet layer_set) {
if (layer_set == this.parent) return;
this.parent = layer_set;
//Utils.log("Layer " +id + ": I have as new parent the LayerSet " + layer_set.getId());
}
public void setParent(final LayerSet layer_set) { // can be null
if (layer_set == this.parent) return;
this.parent = layer_set;
updateInDatabase("layer_set_id");
}
public LayerSet getParent() {
return parent;
}
public double getZ() { return z; }
public double getThickness() { return thickness; }
public double getCalibratedZ() {
return z * parent.getCalibration().pixelWidth; // not pixelDepth ...
}
public double getCalibratedThickness() {
return thickness * parent.getCalibration().pixelWidth; // not pixelDepth ...
}
/** Remove this layer and all its contents from the project. */
@Override
public boolean remove(final boolean check) {
try {
if (check && !Utils.check("Really delete " + this.toString() + " and all its children?")) return false;
// destroy the Display objects that show this layer
Display.remove(this);
// proceed to remove all the children
final Displayable[] displ = new Displayable[al_displayables.size()]; // to avoid concurrent modifications
al_displayables.toArray(displ);
for (int i=0; i<displ.length; i++) {
if (!displ[i].remove2(false)) { // will call back Layer.remove(Displayable)
Utils.log("Could not delete " + displ[i]);
return false;
}
}
al_displayables.clear();
// remove from the parent
/*can't ever be null//if (null != parent) */
parent.remove(this);
Display.updateLayerScroller(parent);
removeFromDatabase();
} catch (final Exception e) { IJError.print(e); return false; }
return true;
}
public void setZ(final double z) {
if (Double.isNaN(z) || z == this.z) return;
this.z = z;
if (null != parent) {
parent.reposition(this);
// fix ordering in the trees (must be done after repositioning in the parent)
final LayerThing lt = project.findLayerThing(this);
if (null != lt) {
final LayerThing p = (LayerThing)lt.getParent();
if (null != p) {
p.removeChild(lt); // does not affect the database
p.addChild(lt); // idem
}
}
}
updateInDatabase("z");
}
public void setThickness(final double thickness) {
if (Double.isNaN(thickness) || thickness == this.thickness) return;
this.thickness = thickness;
updateInDatabase("thickness");
}
public boolean contains(final int x, final int y, int inset) {
if (inset < 0) inset = -inset;
return x >= inset && y >= inset && x <= parent.getLayerWidth() - inset && y <= parent.getLayerHeight() - inset;
}
public boolean contains(final Displayable displ) {
return -1 != al_displayables.indexOf(displ);
}
/** Returns true if any of the Displayable objects are of the given class. */
public boolean contains(final Class<?> c) {
for (final Object ob : al_displayables) {
if (ob.getClass() == c) return true;
}
return false;
}
/** Returns true if any of the Displayable objects are of the given class; if {@code visible_only} is true,
* will return true only if at least one of the matched objects is visible. */
public boolean contains(final Class<?> c, final boolean visible_only) {
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
if (d.getClass() == c) return true;
}
return false;
}
/** Count instances of the given Class. */
public int count(final Class<?> c) {
int n = 0;
for (final Object ob : al_displayables) {
if (ob.getClass() == c) n++;
}
return n;
}
/** Checks if there are any Displayable or if any ZDisplayable paints in this layer. */
public boolean isEmpty() {
return 0 == al_displayables.size() && parent.isEmptyAt(this); // check for ZDisplayable painting here as well
}
/** Returns a copy of the list of Displayable objects.*/
synchronized public ArrayList<Displayable> getDisplayables() {
return new ArrayList<Displayable>(al_displayables);
}
/** Returns the real list of displayables, not a copy. If you modify this list, Thor may ground you with His lightning. */
@Override
public final ArrayList<Displayable> getDisplayableList() {
return al_displayables;
}
synchronized public int getNDisplayables() {
return al_displayables.size();
}
/** Returns a list of Displayable of class c only.*/
synchronized public<T extends Displayable> ArrayList<T> getAll(final Class<T> c) {
// So yes, it can be done to return a typed list of any kind: this WORKS:
final ArrayList<T> al = new ArrayList<T>();
if (null == c) return al;
if (Displayable.class == c) {
al.addAll((Collection<T>)al_displayables); // T is Displayable
return al;
}
for (final Displayable d : al_displayables) {
if (d.getClass() == c) al.add((T)d);
}
return al;
}
/** Returns a list of Displayable of class c only.*/
synchronized public ArrayList<Displayable> getDisplayables(final Class<?> c) {
final ArrayList<Displayable> al = new ArrayList<Displayable>();
if (null == c) return al;
if (Displayable.class == c) {
al.addAll(al_displayables);
return al;
}
for (final Displayable d : al_displayables) {
if (d.getClass() == c) al.add(d);
}
return al;
}
synchronized public ArrayList<Displayable> getDisplayables(final Class<?> c, final boolean visible_only, final boolean instance_of) {
final ArrayList<Displayable> al = new ArrayList<Displayable>();
if (null == c) return al;
if (instance_of) {
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
if (c.isAssignableFrom(d.getClass())) al.add(d);
}
} else {
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
if (d.getClass() == c) al.add(d);
}
}
return al;
}
/** Returns a list of all Displayable of class c that intersect the given rectangle. */
public Collection<Displayable> getDisplayables(final Class<?> c, final Rectangle roi) {
return getDisplayables(c, new Area(roi), true, false);
}
/** Returns a list of all Displayable of class c that intersect the given area. */
synchronized public Collection<Displayable> getDisplayables(final Class<?> c, final Area aroi, final boolean visible_only) {
return getDisplayables(c, aroi, visible_only, false);
}
/** Check class identity by instanceof instead of equality. */
synchronized public Collection<Displayable> getDisplayables(final Class<?> c, final Area aroi, final boolean visible_only, final boolean instance_of) {
if (null != root) return root.find(c, aroi, this, visible_only, instance_of);
// Else, the slow way
final ArrayList<Displayable> al = new ArrayList<Displayable>();
if (Displayable.class == c) {
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
final Area area = d.getArea();
area.intersect(aroi);
final Rectangle b = area.getBounds();
if (!(0 == b.width || 0 == b.height)) al.add(d);
}
return al;
}
if (instance_of) {
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
if (c.isAssignableFrom(d.getClass())) {
final Area area = d.getArea();
area.intersect(aroi);
final Rectangle b = area.getBounds();
if (!(0 == b.width || 0 == b.height)) al.add(d);
}
}
} else {
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
if (d.getClass() == c) {
final Area area = d.getArea();
area.intersect(aroi);
final Rectangle b = area.getBounds();
if (!(0 == b.width || 0 == b.height)) al.add(d);
}
}
}
return al;
}
/** Check class identity with equality, so no superclasses or interfaces are possible. */
synchronized public ArrayList<Displayable> getDisplayables(final Class<?> c, final boolean visible_only) {
final ArrayList<Displayable> al = new ArrayList<Displayable>();
for (final Displayable d : al_displayables) {
if (d.getClass() == c) {
if (visible_only && !d.isVisible()) continue;
al.add(d);
}
}
return al;
}
public Displayable get(final long id) {
for (final Displayable d : al_displayables) {
if (d.getId() == id) return d;
}
return null;
}
@Override
public float getLayerWidth() {
return parent.getLayerWidth();
}
@Override
public float getLayerHeight() {
return parent.getLayerHeight();
}
public Collection<Displayable> find(final double x, final double y) {
return find(x, y, false);
}
/** Find the Displayable objects that contain the point. */
synchronized public Collection<Displayable> find(final double x, final double y, final boolean visible_only) {
if (null != root) return root.find(x, y, this, visible_only);
final ArrayList<Displayable> al = new ArrayList<Displayable>();
for (int i = al_displayables.size() -1; i>-1; i--) {
final Displayable d = (Displayable)al_displayables.get(i);
if (visible_only && !d.isVisible()) continue;
if (d.contains(x, y)) {
al.add(d);
}
}
return al;
}
public Collection<Displayable> find(final Class<?> c, final double x, final double y) {
return find(c, x, y, false, false);
}
/** Find the Displayable objects of Class c that contain the point, with class equality. */
synchronized public Collection<Displayable> find(final Class<?> c, final double x, final double y, final boolean visible_only) {
return find(c, x, y, visible_only, false);
}
/** Find the Displayable objects of Class c that contain the point, with instanceof if instance_of is true. */
synchronized public Collection<Displayable> find(final Class<?> c, final double x, final double y, final boolean visible_only, final boolean instance_of) {
if (null != root) return root.find(c, x, y, this, visible_only, instance_of);
if (Displayable.class == c) return find(x, y, visible_only); // search among all
final ArrayList<Displayable> al = new ArrayList<Displayable>();
for (int i = al_displayables.size() -1; i>-1; i--) {
final Displayable d = al_displayables.get(i);
if (visible_only && !d.isVisible()) continue;
if (d.getClass() == c && d.contains(x, y)) {
al.add(d);
}
}
return al;
}
public Collection<Displayable> find(final Rectangle r) {
return find(r, false);
}
/** Find the Displayable objects whose bounding box intersects with the given rectangle. */
synchronized public Collection<Displayable> find(final Rectangle r, final boolean visible_only) {
if (null != root && root.isBetter(r, this)) return root.find(r, this, visible_only);
final ArrayList<Displayable> al = new ArrayList<Displayable>();
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
if (d.getBoundingBox().intersects(r)) {
al.add(d);
}
}
return al;
}
synchronized public Collection<Displayable> find(final Class<?> c, final Rectangle r, final boolean visible_only) {
return find(c, r, visible_only, false);
}
/** Find the Displayable objects whose bounding box intersects with the given rectangle. */
synchronized public Collection<Displayable> find(final Class<?> c, final Rectangle r, final boolean visible_only, final boolean instance_of) {
if (Displayable.class == c) return find(r, visible_only);
if (null != root && root.isBetter(r, this)) return root.find(c, r, this, visible_only, instance_of);
final ArrayList<Displayable> al = new ArrayList<Displayable>();
for (final Displayable d : al_displayables) {
if (visible_only && !d.isVisible()) continue;
if (d.getClass() != c) continue;
if (d.getBoundingBox().intersects(r)) {
al.add(d);
}
}
return al;
}
/** Find the Displayable objects of class 'target' whose perimeter (not just the bounding box)
* intersect the given Displayable (which is itself included if present in this very Layer). */
synchronized public <T extends Displayable> Collection<T> getIntersecting(final Displayable d, final Class<T> target) {
if (null != root) {
final Area area = new Area(d.getPerimeter());
if (root.isBetter(area.getBounds(), this)) {
return (Collection<T>) root.find(target, area, this, false, true);
}
}
final ArrayList<T> al = new ArrayList<T>();
for (int i = al_displayables.size() -1; i>-1; i--) {
final Object ob = al_displayables.get(i);
if (target.isAssignableFrom(ob.getClass())) continue;
final Displayable da = (Displayable)ob;
if (d.intersects(da)) {
al.add((T)da);
}
}
return al;
}
/** Returns -1 if not found. */
public final int indexOf(final Displayable d) {
return al_displayables.indexOf(d);
}
/** Within its own class only.
* 'up' is at the last element of the ArrayList (since when painting, the first one gets painted first, and thus gets buried the most while the last paints last, on top). */
public void moveUp(final Displayable d) {
final int i = al_displayables.indexOf(d);
if (null == d || -1 == i || al_displayables.size() -1 == i) return;
if (al_displayables.get(i+1).getClass() == d.getClass()) {
//swap
al_displayables.remove(d);
al_displayables.add(i+1, d);
} else return;
updateInDatabase("stack_index");
Display.updatePanelIndex(d.getLayer(), d);
if (null != root) root.updateRange(this, d, i, i+1);
}
/** Within its own class only. */
public void moveDown(final Displayable d) {
final int i = al_displayables.indexOf(d);
if (null == d || -1 == i || 0 == i) return;
if (al_displayables.get(i-1).getClass() == d.getClass()) {
//swap
final Displayable o = al_displayables.remove(i-1);
al_displayables.add(i, o);
} else return;
updateInDatabase("stack_index");
Display.updatePanelIndex(d.getLayer(), d);
if (null != root) root.updateRange(this, d, i-1, i);
}
/** Within its own class only. */
public void moveTop(final Displayable d) { // yes I could have made several lists and make my life easier. Whatever
final int i = al_displayables.indexOf(d);
final int size = al_displayables.size();
if (null == d || -1 == i || size -1 == i) return;
final Class<?> c = d.getClass();
boolean done = false;
int j = i + 1;
for (; j<size; j++) {
if (al_displayables.get(j).getClass() == c) continue;
else {
al_displayables.remove(d);
al_displayables.add(--j, d); // j-1
done = true;
break;
}
}
// solves case of no other class present
if (!done) {
//add at the end
al_displayables.remove(d);
al_displayables.add(d);
j = size-1;
}
updateInDatabase("stack_index");
Display.updatePanelIndex(d.getLayer(), d);
if (null != root) root.updateRange(this, d, i, j);
}
/** Within its own class only. */
public void moveBottom(final Displayable d) {
final int i = al_displayables.indexOf(d);
if (null == d || -1 == i || 0 == i) return;
final Class<?> c = d.getClass();
boolean done = false;
int j = i - 1;
for (; j > -1; j--) {
if (al_displayables.get(j).getClass() == c) continue;
else {
al_displayables.remove(d);
al_displayables.add(++j, d); // j+1
done = true;
break;
}
}
// solve case of no other class present
if (!done) {
al_displayables.remove(d);
al_displayables.add(0, d);
j = 0;
}
updateInDatabase("stack_index");
Display.updatePanelIndex(d.getLayer(), d);
if (null != root) root.updateRange(this, d, j, i);
}
/** Within its own class only. */
public boolean isTop(final Displayable d) {
final int i = al_displayables.indexOf(d);
final int size = al_displayables.size();
if (size -1 == i) return true;
if (al_displayables.get(i+1).getClass() == d.getClass()) return false;
return true;
} // these two methods will throw an Exception if the Displayable is not found (-1 == i) (the null.getClass() *should* throw it)
/** Within its own class only. */
public boolean isBottom(final Displayable d) {
final int i = al_displayables.indexOf(d);
if (0 == i) return true;
if (al_displayables.get(i-1).getClass() == d.getClass()) return false;
return true;
}
/** Get the index of the given Displayable relative to the rest of its class. Beware that the order of the al_displayables is bottom at zero, top at last, but the relative index returned here is inverted: top at zero, bottom at last -to match the tabs' vertical orientation in a Display.*/
public int relativeIndexOf(final Displayable d) {
final int k = al_displayables.indexOf(d);
if (-1 == k) return -1;
final Class<?> c = d.getClass();
final int size = al_displayables.size();
int i = k+1;
for (; i<size; i++) {
if (al_displayables.get(i).getClass() == c) continue;
else {
return i - k -1;
}
}
if (i == size) {
return i - k - 1;
}
Utils.log2("relativeIndexOf: return 0");
return 0;
}
/** Note: Not recursive into embedded LayerSet objects. 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();
if (type.equals("image")) type = "patch";
final HashSet<Displayable> hs = new HashSet<Displayable>();
for (final Displayable d : al_displayables) {
if (visible != d.isVisible() && d.getClass().getName().toLowerCase().endsWith(type)) {
d.setVisible(visible, false); // don't repaint
hs.add(d);
}
}
if (repaint) {
Display.repaint(this);
}
Display.updateCheckboxes(hs, DisplayablePanel.VISIBILITY_STATE, visible);
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 Displayable d : al_displayables) {
if (!d.isVisible()) {
d.setVisible(true, repaint);
col.add(d);
}
}
return col;
}
/** 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(final ArrayList<Class<?>> type, final boolean repaint) {
final HashSet<Displayable> hs = new HashSet<Displayable>();
for (final Displayable d : al_displayables) {
if (!type.contains(d.getClass()) && d.isVisible()) {
d.setVisible(false, repaint);
hs.add(d);
}
}
return hs;
}
@Override
public void exportXML(final StringBuilder sb_body, final String indent, final XMLOptions options) {
final String in = indent + "\t";
// 1 - open tag
sb_body.append(indent).append("<t2_layer oid=\"").append(id).append("\"\n")
.append(in).append(" thickness=\"").append(thickness).append("\"\n")
.append(in).append(" z=\"").append(z).append("\"\n")
;
// TODO this search is linear!
final LayerThing lt = project.findLayerThing(this);
String title;
if (null == lt) title = null;
else title = lt.getTitle();
if (null == title) title = "";
sb_body.append(in).append(" title=\"").append(title).append("\"\n"); // TODO 'title' should be a property of the Layer, not the LayerThing. Also, the LayerThing should not exist: LayerSet and Layer should be directly presentable in a tree. They are not Things as in "objects of the sample", but rather, structural necessities such as Patch.
sb_body.append(indent).append(">\n");
// 2 - export children
if (null != al_displayables) {
for (final Displayable d : al_displayables) {
d.exportXML(sb_body, in, options);
}
}
// 3 - close tag
sb_body.append(indent).append("</t2_layer>\n");
}
/** Includes all Displayable objects in the list of possible children. */
static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
final String type = "t2_layer";
if (hs.contains(type)) return;
hs.add(type);
sb_header.append(indent).append("<!ELEMENT t2_layer (t2_patch,t2_label,t2_layer_set,t2_profile)>\n")
.append(indent).append(Displayable.TAG_ATTR1).append(type).append(" oid").append(Displayable.TAG_ATTR2)
.append(indent).append(Displayable.TAG_ATTR1).append(type).append(" thickness").append(Displayable.TAG_ATTR2)
.append(indent).append(Displayable.TAG_ATTR1).append(type).append(" z").append(Displayable.TAG_ATTR2)
;
}
protected String getLayerThingTitle() {
final LayerThing lt = project.findLayerThing(this);
if (null == lt || null == lt.getTitle() || 0 == lt.getTitle().trim().length()) return "";
return lt.getTitle();
}
@Override
public String getTitle() {
final LayerThing lt = project.findLayerThing(this);
if (null == lt || null == lt.getTitle() || 0 == lt.getTitle().trim().length()) return this.toString();
return lt.getTitle();
}
public void destroy() {
for (final Displayable d : al_displayables) {
d.destroy();
}
}
/** Returns null if no Displayable objects of class c exist. */
public Rectangle getMinimalBoundingBox(final Class<?> c) {
return getMinimalBoundingBox(c, true);
}
/** Returns null if no Displayable objects of class c exist (or are visible if {@code visible_only} is true). */
public Rectangle getMinimalBoundingBox(final Class<?> c, final boolean visible_only) {
Rectangle box = null;
Rectangle tmp = new Rectangle();
for (final Displayable d : getDisplayables(c, visible_only)) {
tmp = d.getBoundingBox(tmp);
if (null == box) {
box = (Rectangle)tmp.clone();
continue;
}
box.add(tmp);
}
return box;
}
/** Returns an Area in world coordinates that represents the inside of all Patches. */
public Area getPatchArea(final boolean visible_only) {
final Area area = new Area(); // with width,height zero
for (final Patch p: getAll(Patch.class)) {
if (visible_only && p.isVisible()) {
area.add(p.getArea());
}
}
return area;
}
/** Preconcatenate the given AffineTransform to all Displayable objects of class c, without respecting their links. */
public void apply(final Class<?> c, final AffineTransform at) {
final boolean all = Displayable.class == c;
for (final Displayable d : al_displayables) {
if (all || d.getClass() == c) {
d.at.preConcatenate(at);
}
}
recreateBuckets();
}
/** Make a copy of this layer into the given LayerSet, enclosing only Displayable objects within the roi, and translating them for that roi x,y. */
public Layer clone(final Project pr, final LayerSet ls, final Rectangle roi, final boolean copy_id, final boolean ignore_hidden_patches) {
final long nid = copy_id ? this.id : pr.getLoader().getNextId();
final Layer copy = new Layer(pr, nid, z, thickness);
copy.parent = ls;
for (final Displayable d : find(roi)) {
if (ignore_hidden_patches && !d.isVisible() && d.getClass() == Patch.class) continue;
copy.addSilently(d.clone(pr, copy_id));
}
final AffineTransform transform = new AffineTransform();
transform.translate(-roi.x, -roi.y);
copy.apply(Displayable.class, transform);
return copy;
}
static public final int IMAGEPROCESSOR = 0;
static public final int PIXELARRAY = 1;
static public final int IMAGE = 2;
static public final int IMAGEPLUS = 3;
/** 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.IMAGEPROCESSOR, Layer.IMAGEPLUS, Layer.PIXELARRAY or Layer.IMAGE.
*/
public Object grab(final Rectangle r, final double scale, final Class<?> c, final int c_alphas, final int format, final int type) {
//Ensure some memory is free
project.getLoader().releaseToFit(r.width, r.height, type, 1.1f);
if (IMAGE == format) {
return project.getLoader().getFlatAWTImage(this, r, scale, c_alphas, type, c, null, true, Color.black);
} else {
final ImagePlus imp = project.getLoader().getFlatImage(this, r, scale, c_alphas, type, c, null, true);
switch (format) {
case IMAGEPLUS:
return imp;
case IMAGEPROCESSOR:
return imp.getProcessor();
case PIXELARRAY:
return imp.getProcessor().getPixels();
}
}
return null;
}
public DBObject findById(final long id) {
if (this.id == id) return this;
for (final Displayable d : al_displayables) {
if (d.getId() == id) return d;
}
return null;
}
// private to the package
void linkPatchesR() {
for (final Displayable d : al_displayables) {
if (d.getClass() == LayerSet.class) ((LayerSet)d).linkPatchesR();
d.linkPatches(); // Patch.class does nothing
}
}
/** Recursive into nested LayerSet objects.*/
public void updateLayerTree() {
project.getLayerTree().addLayer(parent, this);
for (final Displayable d : getDisplayables(LayerSet.class)) {
((LayerSet)d).updateLayerTree();
}
}
/** Don't use this for fast pixel grabbing; this is intended for the dropper tool and status bar reporting by mouse motion. */
public int[] getPixel(final int x, final int y, final double mag) {
// find Patch under cursor
final Collection<Displayable> under = find(Patch.class, x, y);
if (null == under || under.isEmpty()) return new int[3]; // zeros
final Patch pa = (Patch)under.iterator().next();// get(0) // the top one, since they are ordered like html divs
// TODO: edit here when adding layer mipmaps
return pa.getPixel(x, y, mag);
}
synchronized public void recreateBuckets() {
this.root = new Bucket(0, 0, (int)(0.00005 + getLayerWidth()), (int)(0.00005 + getLayerHeight()), Bucket.getBucketSide(this, this));
this.db_map = new HashMap<Displayable,HashSet<Bucket>>();
this.root.populate(this, this, db_map);
//root.debug();
}
/** Update buckets of a position change for the given Displayable. */
@Override
public void updateBucket(final Displayable d, final Layer layer) { // ignore layer
if (null != root) root.updatePosition(d, this, db_map);
}
public void checkBuckets() {
if (use_buckets && (null == root || null == db_map)) recreateBuckets();
}
private boolean use_buckets = true;
public void setBucketsEnabled(final boolean b) {
this.use_buckets = b;
if (!use_buckets) this.root = null;
}
static class DoEditLayer implements DoStep {
final double z, thickness;
final Layer la;
DoEditLayer(final Layer layer) {
this.la = layer;
this.z = layer.z;
this.thickness = layer.thickness;
}
@Override
public Displayable getD() { return null; }
@Override
public boolean isEmpty() { return false; }
@Override
public boolean isIdenticalTo(final Object ob) {
if (!(ob instanceof Layer)) return false;
final Layer layer = (Layer) ob;
return this.la.id == layer.id && this.z == layer.z && this.thickness == layer.thickness;
}
@Override
public boolean apply(final int action) {
la.z = this.z;
la.thickness = this.thickness;
la.getProject().getLayerTree().updateUILater();
Display.update(la.getParent());
return true;
}
}
static class DoEditLayers implements DoStep {
final ArrayList<DoEditLayer> all = new ArrayList<DoEditLayer>();
DoEditLayers(final List<Layer> all) {
for (final Layer la : all) {
this.all.add(new DoEditLayer(la));
}
}
@Override
public Displayable getD() { return null; }
@Override
public boolean isEmpty() { return all.isEmpty(); }
@Override
public boolean isIdenticalTo(final Object ob) {
if (!(ob instanceof DoEditLayers)) return false;
final DoEditLayers other = (DoEditLayers) ob;
if (all.size() != other.all.size()) return false;
// Order matters:
final Iterator<DoEditLayer> it1 = all.iterator();
final Iterator<DoEditLayer> it2 = other.all.iterator();
for (; it1.hasNext() && it2.hasNext(); ) {
if (!it1.next().isIdenticalTo(it2.next())) return false;
}
return true;
}
@Override
public boolean apply(final int action) {
boolean failed = false;
for (final DoEditLayer one : all) {
if (!one.apply(action)) {
failed = true;
}
}
return !failed;
}
}
/** Add an object that is Layer bound, such as Profile, Patch, DLabel and LayerSet. */
static class DoContentChange implements DoStep {
final Layer la;
final ArrayList<Displayable> al;
DoContentChange(final Layer la) {
this.la = la;
this.al = la.getDisplayables(); // a copy
}
@Override
public Displayable getD() { return null; }
@Override
public boolean isEmpty() { return false; }
/** Check that the Displayable objects of this layer are the same and in the same quantity and order. */
@Override
public boolean isIdenticalTo(final Object ob) {
if (!(ob instanceof DoContentChange)) return false;
final DoContentChange dad = (DoContentChange) ob;
if (la != dad.la || al.size() != dad.al.size()) return false;
// Order matters:
final Iterator<Displayable> it1 = al.iterator();
final Iterator<Displayable> it2 = dad.al.iterator();
for (; it1.hasNext() && it2.hasNext(); ) {
if (it1.next() != it2.next()) return false;
}
return true;
}
@Override
public boolean apply(final int action) {
// find the subset in la.al_displayables that is not in this.al
final HashSet<Displayable> sub1 = new HashSet<Displayable>(la.al_displayables);
sub1.removeAll(this.al);
// find the subset in this.al that is not in la.al_displayables
final HashSet<Displayable> sub2 = new HashSet<Displayable>(this.al);
sub2.removeAll(la.al_displayables);
HashSet<Displayable> subA=null, subB=null;
if (action == DoStep.UNDO) {
subA = sub1;
subB = sub2;
} else if (action == DoStep.REDO) {
subA = sub2;
subB = sub1;
}
if (null != subA && null != subB) {
// Mark Patch for mipmap file removal
for (final Displayable d: subA) {
if (d.getClass() == Patch.class) {
d.getProject().getLoader().queueForMipmapRemoval((Patch)d, true);
}
}
// ... or unmark:
for (final Displayable d: subB) {
if (d.getClass() == Patch.class) {
d.getProject().getLoader().queueForMipmapRemoval((Patch)d, false);
}
}
}
la.al_displayables.clear();
la.al_displayables.addAll(this.al);
la.recreateBuckets();
Display.updateVisibleTabs();
Display.clearSelection();
Display.update(la);
return true;
}
}
static protected class DoMoveDisplayable implements DoStep {
final ArrayList<Displayable> al_displayables;
final Layer layer;
HashSet<DoStep> dependents = null;
DoMoveDisplayable(final Layer layer) {
this.layer = layer;
this.al_displayables = new ArrayList<Displayable>(layer.al_displayables);
}
@Override
public boolean apply(final int action) {
// Replace all ZDisplayable
layer.al_displayables.clear();
layer.al_displayables.addAll(this.al_displayables);
Display.update(layer);
return true;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public Displayable getD() {
return null;
}
@Override
public boolean isIdenticalTo(final Object ob) {
if (!(ob instanceof DoMoveDisplayable)) return false;
final DoMoveDisplayable dm = (DoMoveDisplayable)ob;
if (dm.layer != this.layer) return false;
if (dm.al_displayables.size() != this.al_displayables.size()) return false;
for (int i=0; i<this.al_displayables.size(); ++i) {
if (dm.al_displayables.get(i) != this.al_displayables.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) {
final Overlay old = this.overlay;
this.overlay = o;
return old;
}
@Override
public int compareTo(final Layer layer) {
final double diff = this.z - layer.z;
if (diff < 0) return -1;
if (diff > 0) return 1;
return 0;
}
/** Transfer the world coordinate specified by {@code world_x},{@code world_y}
* in pixels, to the local coordinate of the {@link Patch} immediately present under it.
* @return null if no {@link Patch} is under the coordinate, else the {@link Coordinate} with the x, y, {@link Layer} and the {@link Patch}.
* @throws NoninvertibleModelException
* @throws NoninvertibleTransformException
*/
public Coordinate<Patch> toPatchCoordinate(final double world_x, final double world_y) throws NoninvertibleTransformException, NoninvertibleModelException {
final Collection<Displayable> ps = find(Patch.class, world_x, world_y, true, false);
Patch patch = null;
if (ps.isEmpty()) {
// No Patch under the point. Find the nearest Patch instead
final Collection<Patch> patches = getAll(Patch.class);
if (patches.isEmpty()) return null;
double minSqDist = Double.MAX_VALUE;
for (final Patch p : patches) {
// Check if any of the 4 corners of the bounding box are beyond minSqDist
final Rectangle b = p.getBoundingBox();
final double d1 = Math.pow(b.x - world_x, 2) + Math.pow(b.y - world_y, 2),
d2 = Math.pow(b.x + b.width - world_x, 2) + Math.pow(b.y - world_y, 2),
d3 = Math.pow(b.x - world_x, 2) + Math.pow(b.y + b.height - world_y, 2),
d4 = Math.pow(b.x + b.width - world_x, 2) + Math.pow(b.y + b.height - world_y, 2),
d = Math.min(d1, Math.min(d2, Math.min(d3, d4)));
if (d < minSqDist) {
patch = p;
minSqDist = d;
}
// If the Patch has a CoordinateTransform, find the closest perimeter point
if (p.hasCoordinateTransform()) {
for (final Polygon pol : M.getPolygons(p.getArea())) { // Area in world coordinates
for (int i=0; i<pol.npoints; ++i) {
final double sqDist = Math.pow(pol.xpoints[0] - world_x, 2) + Math.pow(pol.ypoints[1] - world_y, 2);
if (sqDist < minSqDist) {
minSqDist = sqDist;
patch = p;
}
}
}
}
}
} else {
patch = (Patch) ps.iterator().next();
}
final double[] point = patch.toPixelCoordinate(world_x, world_y);
return new Coordinate<Patch>(point[0], point[1], patch.getLayer(), patch);
}
}