Now deleting a layer is more gentle on the display: just moves to the
[trakem2.git] / ini / trakem2 / display / Layer.java
blobb82068f1824f69fe527c95cf7de85022a40eaaac
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.display;
26 import ij.gui.GenericDialog;
27 import ij.ImagePlus;
29 import ini.trakem2.ControlWindow;
30 import ini.trakem2.Project;
31 import ini.trakem2.persistence.DBObject;
32 import ini.trakem2.tree.LayerThing;
33 import ini.trakem2.utils.IJError;
34 import ini.trakem2.utils.Utils;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Collection;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.Map;
42 import java.util.Iterator;
43 import java.util.TreeMap;
44 import java.util.Collection;
46 import java.awt.Color;
47 import java.awt.Rectangle;
48 import java.awt.geom.AffineTransform;
49 import java.awt.geom.Area;
50 import java.awt.Image;
52 public final class Layer extends DBObject implements Bucketable {
54 private final ArrayList<Displayable> al_displayables = new ArrayList<Displayable>();
55 /** For fast search. */
56 Bucket root = null;
57 private HashMap<Displayable,ArrayList<Bucket>> db_map = null;
59 private double z;
60 private double thickness;
62 private LayerSet parent;
64 public Layer(Project project, double z, double thickness, LayerSet parent) {
65 super(project);
66 this.z = z;
67 this.thickness = thickness;
68 this.parent = parent;
69 addToDatabase();
72 /** Reconstruct from database*/
73 public Layer(Project project, long id, double z, double thickness) {
74 super(project, id);
75 this.z = z;
76 this.thickness = thickness;
77 this.parent = null;
80 /** Reconstruct from XML file. */
81 public Layer(Project project, long id, HashMap ht_attributes) {
82 super(project, id);
83 this.parent = null;
84 // parse data
85 for (Iterator it = ht_attributes.entrySet().iterator(); it.hasNext(); ) {
86 Map.Entry entry = (Map.Entry)it.next();
87 String key = (String)entry.getKey();
88 String data = (String)entry.getValue();
89 if (key.equals("z")) {
90 this.z = Double.parseDouble(data);
91 } else if (key.equals("thickness")) {
92 this.thickness = Double.parseDouble(data);
94 // all the above could have been done with reflection since the fields have the same name
98 /** 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.*/
99 static public Layer create(Project project, LayerSet parent) {
100 if (null == parent) return null;
101 GenericDialog gd = ControlWindow.makeGenericDialog("New Layer");
102 gd.addMessage("In pixels:"); // TODO set elsewhere the units!
103 gd.addNumericField("z coordinate: ", 0.0D, 3);
104 gd.addNumericField("thickness: ", 1.0D, 3);
105 gd.showDialog();
106 if (gd.wasCanceled()) return null;
107 try {
108 double z = gd.getNextNumber();
109 double thickness = gd.getNextNumber();
110 if (Double.isNaN(z) || Double.isNaN(thickness)) return null;
111 Layer layer = new Layer(project, z, thickness, parent);
112 parent.add(layer);
113 return layer;
114 } catch (Exception e) {}
115 return null;
118 static public Layer[] createMany(Project project, LayerSet parent) {
119 if (null == parent) return null;
120 GenericDialog gd = ControlWindow.makeGenericDialog("Many new layers");
121 gd.addNumericField("First Z coord: ", 0, 3);
122 gd.addNumericField("thickness: ", 1.0, 3);
123 gd.addNumericField("Number of layers: ", 1, 0);
124 gd.addCheckbox("Skip existing layers", true);
125 gd.showDialog();
126 if (gd.wasCanceled()) return null;
127 // start iteration to add layers
128 boolean skip = gd.getNextBoolean();
129 double z = gd.getNextNumber();
130 double thickness = gd.getNextNumber();
131 int n_layers = (int)gd.getNextNumber();
132 if (thickness < 0) {
133 Utils.showMessage("Can't create layers with negative thickness");
134 return null;
136 if (n_layers < 1) {
137 Utils.showMessage("Invalid number of layers");
138 return null;
140 Layer[] layer = new Layer[n_layers];
141 for (int i=0; i<n_layers; i++) {
142 if (skip) {
143 layer[i] = parent.getLayer(z);
144 if (null == layer[i]) {
145 layer[i] = new Layer(project, z, thickness, parent);
146 parent.addSilently(layer[i]);
147 } else layer[i] = null; // don't create
148 } else {
149 layer[i] = new Layer(project, z, thickness, parent);
150 parent.addSilently(layer[i]);
152 z += thickness;
154 // update the scroller of currently open Displays
155 Display.updateLayerScroller(parent);
156 return layer;
159 /** 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. */
160 public String getPrintableTitle() {
161 final LayerThing lt = project.findLayerThing(this);
162 if (null == lt) return toString();
163 String title = lt.getTitle();
164 if (null == title) title = "";
165 else title = title.replace(' ', '_');
166 final StringBuffer sb = new StringBuffer().append(parent.indexOf(this) + 1);
167 final int s_size = Integer.toString(parent.size()).length();
168 while (sb.length() < s_size) {
169 sb.insert(0, '0');
171 sb.append('_').append('_').append(title).append('_').append('_').append('z').append(Utils.cutNumber(this.z, 3, true));
172 return sb.toString();
175 public String toString() {
176 if (null == parent) return new StringBuffer("z=").append(Utils.cutNumber(z, 4)).toString();
177 //return "z=" + Utils.cutNumber(z / parent.getCalibration().pixelDepth * z !!!?? I don't have the actual depth to correct with.
178 return "z=" + Utils.cutNumber(z, 4);
181 /** Add a displayable and update all Display instances showing this Layer. */
182 public void add(Displayable displ) { add(displ, true); }
184 public void add(Displayable displ, boolean update_displays) {
185 add(displ, update_displays, true);
188 public void add(final Displayable displ, final boolean update_displays, final boolean update_db) {
189 if (null == displ || -1 != al_displayables.indexOf(displ)) return;
191 int i=-1, j=-1;
192 final Displayable[] d = new Displayable[al_displayables.size()];
193 al_displayables.toArray(d);
194 int stack_index = 0;
195 // what is it?
196 if (displ instanceof Patch) {
197 // find last Patch (which start at 0)
198 for (i=0; i<d.length; i++) {
199 if (d[i] instanceof Patch) { j = i;}
200 else break;
202 if (-1 != j) {
203 j++;
204 if (j >= d.length) {
205 al_displayables.add(displ); // at the end
206 stack_index = d.length;
207 } else {
208 al_displayables.add(j, displ);
209 stack_index = j;
211 } else {
212 // no patches
213 al_displayables.add(0, displ); // at the very beggining
214 stack_index = 0;
216 } else if (displ instanceof Profile) {
217 // find first LayerSet or if none, first DLabel, add before it
218 for (i=d.length-1; i>-1; i--) {
219 if (! (d[i] instanceof DLabel || d[i] instanceof LayerSet)) { j = i; break; }
221 if (-1 != j) {
222 j++;
223 if (j >= d.length) { al_displayables.add(displ); stack_index = d.length; }
224 else { al_displayables.add(j, displ); stack_index = j; }
225 } else {
226 // no labels or LayerSets
227 al_displayables.add(displ); // at the end
228 stack_index = d.length;
230 } else if (displ instanceof LayerSet) {
231 // find first DLabel, add before it
232 for (i=d.length-1; i>-1; i--) {
233 if (! (d[i] instanceof DLabel)) { j = i; break; }
235 if (-1 != j) {
236 j++; // add it after the non-label one, displacing the label one position
237 if (j >= d.length) { al_displayables.add(displ); stack_index = d.length; } // at the end
238 else { al_displayables.add(j, displ); stack_index = j; }
239 } else {
240 // no labels
241 al_displayables.add(displ); // at the end
242 stack_index = d.length;
244 } else {
245 // displ is a DLabel
246 al_displayables.add(displ); // at the end
247 stack_index = d.length;
250 if (update_db) {
251 updateInDatabase("stack_index"); // of the displayables ...
252 displ.setLayer(this);
253 } else {
254 displ.setLayer(this, false);
257 // insert into bucket
258 if (null != root) {
259 if (d.length == stack_index) {
260 // append at the end
261 root.put(stack_index, displ, displ.getBoundingBox(null));
262 } else {
263 // add as last first, then update
264 root.put(d.length, displ, displ.getBoundingBox(null));
265 // find and update the range of affected Displayable objects
266 root.update(this, displ, stack_index, d.length); // first to last indices affected
270 if (update_displays) {
271 Display.add(this, displ);
275 public HashMap<Displayable, ArrayList<Bucket>> getBucketMap() {
276 return db_map;
279 /** Used for reconstruction purposes. Assumes the displ are given in the proper order! */
280 public void addSilently(final DBObject displ) { // why DBObject and not Displayable ?? TODO
281 if (null == displ || -1 != al_displayables.indexOf(displ)) return;
282 try {
283 ((Displayable)displ).setLayer(this, false);
284 al_displayables.add((Displayable)displ);
285 } catch (Exception e) {
286 Utils.log("Layer.addSilently: Not a Displayable/LayerSet, not adding DBObject id=" + displ.getId());
287 return;
291 public boolean remove(final Displayable displ) {
292 if (null == displ || null == al_displayables || -1 == al_displayables.indexOf(displ)) {
293 Utils.log2("Layer can't remove Displayable " + displ.getId());
294 return false;
296 // remove from Bucket before modifying stack index
297 if (null != root) Bucket.remove(displ, db_map);
298 // now remove proper, so stack_index hasn't changed yet
299 al_displayables.remove(displ);
300 Display.remove(this, displ);
301 return true;
304 /** Used for reconstruction purposes. */
305 public void setParentSilently(LayerSet layer_set) {
306 if (layer_set == this.parent) return;
307 this.parent = layer_set;
308 //Utils.log("Layer " +id + ": I have as new parent the LayerSet " + layer_set.getId());
311 public void setParent(LayerSet layer_set) { // can be null
312 if (layer_set == this.parent) return;
313 this.parent = layer_set;
314 updateInDatabase("layer_set_id");
317 public LayerSet getParent() {
318 return parent;
321 public double getZ() { return z; }
322 public double getThickness() { return thickness; }
324 /** Remove this layer and all its contents from the project. */
325 public boolean remove(boolean check) {
326 try {
327 if (check && !Utils.check("Really delete " + this.toString() + " and all its children?")) return false;
328 // destroy the Display objects that show this layer
329 Display.remove(this);
330 // proceed to remove all the children
331 Displayable[] displ = new Displayable[al_displayables.size()]; // to avoid concurrent modifications
332 al_displayables.toArray(displ);
333 for (int i=0; i<displ.length; i++) {
334 if (!displ[i].remove(false)) { // will call back Layer.remove(Displayable)
335 Utils.showMessage("Could not delete " + displ[i]);
336 return false;
339 al_displayables.clear();
340 // remove from the parent
341 /*can't ever be null//if (null != parent) */
342 parent.remove(this);
343 Display.updateLayerScroller(parent);
344 removeFromDatabase();
345 } catch (Exception e) { IJError.print(e); return false; }
346 return true;
349 public void setZ(double z) {
350 if (Double.isNaN(z) || z == this.z) return;
351 this.z = z;
352 if (null != parent) {
353 parent.reposition(this);
354 // fix ordering in the trees (must be done after repositioning in the parent)
355 LayerThing lt = project.findLayerThing(this);
356 if (null != lt) {
357 LayerThing p = (LayerThing)lt.getParent();
358 if (null != p) {
359 p.removeChild(lt); // does not affect the database
360 p.addChild(lt); // idem
364 updateInDatabase("z");
367 public void setThickness(final double thickness) {
368 if (Double.isNaN(thickness) || thickness == this.thickness) return;
369 this.thickness = thickness;
370 updateInDatabase("thickness");
373 public boolean contains(final int x, final int y, int inset) {
374 if (inset < 0) inset = -inset;
375 return x >= inset && y >= inset && x <= parent.getLayerWidth() - inset && y <= parent.getLayerHeight() - inset;
378 public boolean contains(final Displayable displ) {
379 return -1 != al_displayables.indexOf(displ);
382 /** Returns true if any of the Displayable objects are of the given class. */
383 public boolean contains(final Class c) {
384 for (Object ob : al_displayables) {
385 if (ob.getClass() == c) return true;
387 return false;
390 /** Count instances of the given Class. */
391 public int count(final Class c) {
392 int n = 0;
393 for (Object ob : al_displayables) {
394 if (ob.getClass() == c) n++;
396 return n;
399 public boolean isEmpty() {
400 return 0 == al_displayables.size() && parent.isEmptyAt(this); // check for ZDisplayable painting here as well
403 /** Returns a copy of the list of Displayable objects.*/
404 public ArrayList<Displayable> getDisplayables() {
405 return (ArrayList<Displayable>)al_displayables.clone();
408 /** Returns the real list of displayables, not a copy. If you modify this list, Thor may ground you with His lightning. */
409 public final ArrayList<Displayable> getDisplayableList() {
410 return al_displayables;
413 public int getNDisplayables() {
414 return al_displayables.size();
417 /** Returns a list of Displayable of class c only.*/
418 public ArrayList<Displayable> getDisplayables(final Class c) {
419 final ArrayList<Displayable> al = new ArrayList<Displayable>();
420 if (null == c) return al;
421 if (Displayable.class == c) {
422 al.addAll(al_displayables);
423 return al;
425 for (Object ob : al_displayables) {
426 if (ob.getClass() == c) al.add((Displayable)ob); // cast only the few added, not all as it would in looping with Displayabe d : al_displayables
428 return al;
431 /** Returns a list of all Displayable of class c that intersect the given rectangle. */
432 public ArrayList<Displayable> getDisplayables(final Class c, final Rectangle roi) {
433 return getDisplayables(c, new Area(roi), true);
436 /** Returns a list of all Displayable of class c that intersect the given area. */
437 public ArrayList<Displayable> getDisplayables(final Class c, final Area aroi, final boolean visible_only) {
438 final ArrayList<Displayable> al = getDisplayables(c);
439 for (Iterator<Displayable> it = al.iterator(); it.hasNext(); ) {
440 Displayable d = it.next();
441 if (visible_only && !d.isVisible()) { it.remove(); continue; }
442 Area area = new Area(d.getPerimeter());
443 area.intersect(aroi);
444 Rectangle b = area.getBounds();
445 if (0 == b.width || 0 == b.height) it.remove();
447 return al;
450 public Displayable get(final long id) {
451 for (Displayable d : al_displayables) {
452 if (d.getId() == id) return d;
454 return null;
457 public double getLayerWidth() {
458 return parent.getLayerWidth();
460 public double getLayerHeight() {
461 return parent.getLayerHeight();
464 public Collection<Displayable> find(final int x, final int y) {
465 return find(x, y, false);
468 /** Find the Displayable objects that contain the point. */
469 public Collection<Displayable> find(final int x, final int y, final boolean visible_only) {
470 if (null != root) return root.find(x, y, this, visible_only);
471 final ArrayList<Displayable> al = new ArrayList<Displayable>();
472 for (int i = al_displayables.size() -1; i>-1; i--) {
473 Displayable d = (Displayable)al_displayables.get(i);
474 if (visible_only && !d.isVisible()) continue;
475 if (d.contains(x, y)) {
476 al.add(d);
479 return al;
482 public Collection<Displayable> find(final Class c, final int x, final int y) {
483 return find(c, x, y, false);
486 /** Find the Displayable objects of Class c that contain the point. */
487 public Collection<Displayable> find(final Class c, final int x, final int y, final boolean visible_only) {
488 if (Displayable.class == c) return find(x, y); // search among all
489 final ArrayList<Displayable> al = new ArrayList<Displayable>();
490 for (int i = al_displayables.size() -1; i>-1; i--) {
491 Displayable d = (Displayable)al_displayables.get(i);
492 if (visible_only && !d.isVisible()) continue;
493 if (d.getClass() == c && d.contains(x, y)) {
494 al.add(d);
497 return al;
500 public Collection<Displayable> find(final Rectangle r) {
501 return find(r, false);
504 /** Find the Displayable objects whose bounding box intersects with the given rectangle. */
505 public Collection<Displayable> find(final Rectangle r, final boolean visible_only) {
506 if (null != root && root.isBetter(r, this)) return root.find(r, this, visible_only);
507 final ArrayList<Displayable> al = new ArrayList<Displayable>();
508 for (Displayable d : al_displayables) {
509 if (visible_only && !d.isVisible()) continue;
510 if (d.getBoundingBox().intersects(r)) {
511 al.add(d);
514 return al;
517 /** Find the Displayable objects of class 'target' whose perimeter (not just the bounding box) intersect the given Displayable (which is itself not included if present in this very Layer). */
518 public Collection<Displayable> getIntersecting(final Displayable d, final Class target) {
519 if (null != root) {
520 final Area area = new Area(d.getPerimeter());
521 if (root.isBetter(area.getBounds(), this)) return root.find(area, this, false);
523 final ArrayList<Displayable> al = new ArrayList();
524 for (int i = al_displayables.size() -1; i>-1; i--) {
525 Object ob = al_displayables.get(i);
526 if (ob.getClass() != target) continue;
527 Displayable da = (Displayable)ob;
528 if (d.intersects(da)) {
529 al.add(da);
532 // remove the calling one
533 if (al.contains(d)) al.remove(d);
534 return al;
537 /** Returns -1 if not found. */
538 public final int indexOf(final Displayable d) {
539 return al_displayables.indexOf(d);
542 /** Within its own class only.
543 * '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). */
544 public void moveUp(final Displayable d) {
545 final int i = al_displayables.indexOf(d);
546 if (null == d || -1 == i || al_displayables.size() -1 == i) return;
547 if (al_displayables.get(i+1).getClass() == d.getClass()) {
548 //swap
549 al_displayables.remove(d);
550 al_displayables.add(i+1, d);
551 } else return;
552 updateInDatabase("stack_index");
553 Display.updatePanelIndex(d.getLayer(), d);
554 if (null != root) root.update(this, d, i, i+1);
557 /** Within its own class only. */
558 public void moveDown(final Displayable d) {
559 final int i = al_displayables.indexOf(d);
560 if (null == d || -1 == i || 0 == i) return;
561 if (al_displayables.get(i-1).getClass() == d.getClass()) {
562 //swap
563 Displayable o = al_displayables.remove(i-1);
564 al_displayables.add(i, o);
565 } else return;
566 updateInDatabase("stack_index");
567 Display.updatePanelIndex(d.getLayer(), d);
568 if (null != root) root.update(this, d, i-1, i);
571 /** Within its own class only. */
572 public void moveTop(final Displayable d) { // yes I could have made several lists and make my life easier. Whatever
573 final int i = al_displayables.indexOf(d);
574 final int size = al_displayables.size();
575 if (null == d || -1 == i || size -1 == i) return;
576 final Class c = d.getClass();
577 boolean done = false;
578 int j = i + 1;
579 for (; j<size; j++) {
580 if (al_displayables.get(j).getClass() == c) continue;
581 else {
582 al_displayables.remove(d);
583 al_displayables.add(--j, d); // j-1
584 done = true;
585 break;
588 // solves case of no other class present
589 if (!done) {
590 //add at the end
591 al_displayables.remove(d);
592 al_displayables.add(d);
593 j = size-1;
595 updateInDatabase("stack_index");
596 Display.updatePanelIndex(d.getLayer(), d);
597 if (null != root) root.update(this, d, i, j);
600 /** Within its own class only. */
601 public void moveBottom(final Displayable d) {
602 final int i = al_displayables.indexOf(d);
603 if (null == d || -1 == i || 0 == i) return;
604 Class c = d.getClass();
605 boolean done = false;
606 int j = i - 1;
607 for (; j > -1; j--) {
608 if (al_displayables.get(j).getClass() == c) continue;
609 else {
610 al_displayables.remove(d);
611 al_displayables.add(++j, d); // j+1
612 done = true;
613 break;
616 // solve case of no other class present
617 if (!done) {
618 al_displayables.remove(d);
619 al_displayables.add(0, d);
620 j = 0;
622 updateInDatabase("stack_index");
623 Display.updatePanelIndex(d.getLayer(), d);
624 if (null != root) root.update(this, d, j, i);
627 /** Within its own class only. */
628 public boolean isTop(final Displayable d) {
629 final int i = al_displayables.indexOf(d);
630 final int size = al_displayables.size();
631 if (size -1 == i) return true;
632 if (al_displayables.get(i+1).getClass() == d.getClass()) return false;
633 return true;
634 } // these two methods will throw an Exception if the Displayable is not found (-1 == i) (the null.getClass() *should* throw it)
635 /** Within its own class only. */
636 public boolean isBottom(final Displayable d) {
637 final int i = al_displayables.indexOf(d);
638 if (0 == i) return true;
639 if (al_displayables.get(i-1).getClass() == d.getClass()) return false;
640 return true;
643 /** 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.*/
644 public int relativeIndexOf(final Displayable d) {
645 int k = al_displayables.indexOf(d);
646 if (-1 == k) return -1;
647 Class c = d.getClass();
648 int size = al_displayables.size();
649 int i = k+1;
650 for (; i<size; i++) {
651 if (al_displayables.get(i).getClass() == c) continue;
652 else {
653 return i - k -1;
656 if (i == size) {
657 return i - k - 1;
659 Utils.log2("relativeIndexOf: return 0");
660 return 0;
663 /** Note: Not recursive into embedded LayerSet objects. Returns the hash set of objects whose visibility has changed. */
664 public HashSet<Displayable> setVisible(String type, boolean visible, boolean repaint) {
665 type = type.toLowerCase();
666 if (type.equals("image")) type = "patch";
667 final HashSet<Displayable> hs = new HashSet<Displayable>();
668 for (Displayable d : al_displayables) {
669 if (visible != d.isVisible() && d.getClass().getName().toLowerCase().endsWith(type)) {
670 d.setVisible(visible, false); // don't repaint
671 Display.updateVisibilityCheckbox(this, d, null);
672 hs.add(d);
675 if (repaint) {
676 Display.repaint(this);
678 return hs;
680 public void setAllVisible(boolean repaint) {
681 for (Displayable d : al_displayables) {
682 if (!d.isVisible()) d.setVisible(true, repaint);
686 /** Hide all except those whose type is in 'type' list, whose visibility flag is left unchanged. Returns the list of displayables made hidden. */
687 public HashSet<Displayable> hideExcept(ArrayList<Class> type, boolean repaint) {
688 final HashSet<Displayable> hs = new HashSet<Displayable>();
689 for (Displayable d : al_displayables) {
690 if (!type.contains(d.getClass()) && d.isVisible()) {
691 d.setVisible(false, repaint);
692 hs.add(d);
695 return hs;
698 public void exportXML(StringBuffer sb_body, String indent, Object any) {
699 String in = indent + "\t";
700 // 1 - open tag
701 sb_body.append(indent).append("<t2_layer oid=\"").append(id).append("\"\n")
702 .append(in).append(" thickness=\"").append(thickness).append("\"\n")
703 .append(in).append(" z=\"").append(z).append("\"\n")
705 String title = project.findLayerThing(this).getTitle();
706 if (null == title) title = "";
707 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.
708 sb_body.append(indent).append(">\n");
709 // 2 - export children
710 if (null != al_displayables) {
711 for (Iterator it = al_displayables.iterator(); it.hasNext(); ) {
712 Displayable d = (Displayable)it.next();
713 d.exportXML(sb_body, in, any);
716 // 3 - close tag
717 sb_body.append(indent).append("</t2_layer>\n");
720 /** Includes all Displayable objects in the list of possible children. */
721 static public void exportDTD(StringBuffer sb_header, HashSet hs, String indent) {
722 String type = "t2_layer";
723 if (hs.contains(type)) return;
724 sb_header.append(indent).append("<!ELEMENT t2_layer (t2_patch,t2_label,t2_layer_set,t2_profile)>\n")
725 .append(indent).append(Displayable.TAG_ATTR1).append(type).append(" oid").append(Displayable.TAG_ATTR2)
726 .append(indent).append(Displayable.TAG_ATTR1).append(type).append(" thickness").append(Displayable.TAG_ATTR2)
727 .append(indent).append(Displayable.TAG_ATTR1).append(type).append(" z").append(Displayable.TAG_ATTR2)
731 public String getTitle() {
732 LayerThing lt = project.findLayerThing(this);
733 if (null == lt || null == lt.getTitle() || 0 == lt.getTitle().trim().length()) return this.toString();
734 return lt.getTitle();
737 public void destroy() {
738 for (Iterator it = al_displayables.iterator(); it.hasNext(); ) {
739 Displayable d = (Displayable)it.next();
740 d.destroy();
744 /** Returns null if no Displayable objects of class c exist. */
745 public Rectangle getMinimalBoundingBox(final Class c) {
746 Rectangle box = null;
747 Rectangle tmp = new Rectangle();
748 for (Iterator it = getDisplayables(c).iterator(); it.hasNext(); ) {
749 tmp = ((Displayable)it.next()).getBoundingBox(tmp);
750 if (null == box) {
751 box = (Rectangle)tmp.clone();
752 continue;
754 box.add(tmp);
756 return box;
759 /** Preconcatenate the given AffineTransform to all Displayable objects of class c, without respecting their links. */
760 public void apply(final Class c, final AffineTransform at) {
761 boolean all = Displayable.class == c;
762 for (Iterator it = al_displayables.iterator(); it.hasNext(); ) {
763 final Displayable d = (Displayable)it.next();
764 if (all || d.getClass() == c) {
765 d.preTransform(at, false);
768 recreateBuckets();
771 /** 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. */
772 public Layer clone(final Project pr, LayerSet ls, final Rectangle roi, final boolean copy_id) {
773 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
774 final Layer copy = new Layer(pr, nid, z, thickness);
775 copy.parent = ls;
776 for (Iterator it = find(roi).iterator(); it.hasNext(); ) {
777 Displayable dc = ((Displayable)it.next()).clone(pr, copy_id);
778 copy.addSilently(dc);
780 final AffineTransform transform = new AffineTransform();
781 transform.translate(-roi.x, -roi.y);
782 copy.apply(Displayable.class, transform);
783 return copy;
786 static public final int IMAGEPROCESSOR = 0;
787 static public final int PIXELARRAY = 1;
788 static public final int IMAGE = 2;
789 static public final int IMAGEPLUS = 3;
791 /** Returns the region defined by the rectangle as an image in the type and format specified.
792 * The type is either ImagePlus.GRAY8 or ImagePlus.COLOR_RGB.
793 * The format is either Layer.IMAGEPROCESSOR, Layer.IMAGEPLUS, Layer.PIXELARRAY or Layer.IMAGE.
795 public Object grab(final Rectangle r, final double scale, final Class c, final int c_alphas, final int format, final int type) {
796 // check that it will fit in memory
797 if (!project.getLoader().releaseToFit(r.width, r.height, type, 1.1f)) {
798 Utils.log("Layer.grab: Cannot fit a flat image of " + (long)(r.width*r.height*(ImagePlus.GRAY8==type?1:4)*1.1) + " bytes in memory.");
799 return null;
801 if (IMAGE == format) {
802 return project.getLoader().getFlatAWTImage(this, r, scale, c_alphas, type, c, null, true, Color.black);
803 } else {
804 final ImagePlus imp = project.getLoader().getFlatImage(this, r, scale, c_alphas, type, c, null, true);
805 switch (format) {
806 case IMAGEPLUS:
807 return imp;
808 case IMAGEPROCESSOR:
809 return imp.getProcessor();
810 case PIXELARRAY:
811 return imp.getProcessor().getPixels();
814 return null;
817 public DBObject findById(final long id) {
818 if (this.id == id) return this;
819 for (Displayable d : al_displayables) {
820 if (d.getId() == id) return d;
822 return null;
825 // private to the package
826 void linkPatchesR() {
827 for (Displayable d : al_displayables) {
828 if (d.getClass() == LayerSet.class) ((LayerSet)d).linkPatchesR();
829 d.linkPatches(); // Patch.class does nothing
833 /** Recursive into nested LayerSet objects.*/
834 public void updateLayerTree() {
835 project.getLayerTree().addLayer(parent, this);
836 for (Displayable d : getDisplayables(LayerSet.class)) {
837 ((LayerSet)d).updateLayerTree();
841 /** Don't use this for fast pixel grabbing; this is intended for the dropper tool and status bar reporting by mouse motion. */
842 public int[] getPixel(final int x, final int y, final double mag) {
843 // find Patch under cursor
844 final Collection<Displayable> under = find(Patch.class, x, y);
845 if (null == under || under.isEmpty()) return new int[3]; // zeros
846 final Patch pa = (Patch)under.iterator().next();// get(0) // the top one, since they are ordered like html divs
847 // TODO: edit here when adding layer mipmaps
848 return pa.getPixel(x, y, mag);
851 public void recreateBuckets() {
852 this.root = new Bucket(0, 0, (int)(0.00005 + getLayerWidth()), (int)(0.00005 + getLayerHeight()), Bucket.getBucketSide(this));
853 this.db_map = new HashMap<Displayable,ArrayList<Bucket>>();
854 this.root.populate(this, db_map);
855 //root.debug();
858 /** Update buckets of a position change for the given Displayable. */
859 public void updateBucket(final Displayable d) {
860 if (null != root) root.updatePosition(d, db_map);
863 public void checkBuckets() {
864 if (use_buckets && (null == root || null == db_map)) recreateBuckets();
867 private boolean use_buckets = true;
869 public void setBucketsEnabled(boolean b) {
870 this.use_buckets = b;
871 if (!use_buckets) this.root = null;
874 static class DoEditLayer implements DoStep {
875 final double z, thickness;
876 final Layer la;
877 DoEditLayer(final Layer layer) {
878 this.la = layer;
879 this.z = layer.z;
880 this.thickness = layer.thickness;
882 public Displayable getD() { return null; }
883 public boolean isEmpty() { return false; }
884 public boolean isIdenticalTo(final Object ob) {
885 if (!(ob instanceof Layer)) return false;
886 final Layer layer = (Layer) ob;
887 return this.la.id == layer.id && this.z == layer.z && this.thickness == layer.thickness;
889 public boolean apply(int action) {
890 la.z = this.z;
891 la.thickness = this.thickness;
892 la.getProject().getLayerTree().updateUILater();
893 Display.update(la.getParent());
894 return true;
898 static class DoEditLayers implements DoStep {
899 final ArrayList<DoEditLayer> all = new ArrayList<DoEditLayer>();
900 DoEditLayers(final List<Layer> all) {
901 for (final Layer la : all) {
902 this.all.add(new DoEditLayer(la));
905 public Displayable getD() { return null; }
906 public boolean isEmpty() { return all.isEmpty(); }
907 public boolean isIdenticalTo(final Object ob) {
908 if (!(ob instanceof DoEditLayers)) return false;
909 final DoEditLayers other = (DoEditLayers) ob;
910 if (all.size() != other.all.size()) return false;
911 // Order matters:
912 final Iterator<DoEditLayer> it1 = all.iterator();
913 final Iterator<DoEditLayer> it2 = other.all.iterator();
914 for (; it1.hasNext() && it2.hasNext(); ) {
915 if (!it1.next().isIdenticalTo(it2.next())) return false;
917 return true;
919 public boolean apply(int action) {
920 boolean failed = false;
921 for (final DoEditLayer one : all) {
922 if (!one.apply(action)) {
923 failed = true;
926 return !failed;
930 /** Add an object that is Layer bound, such as Profile, Patch, DLabel and LayerSet. */
931 static class DoContentChange implements DoStep {
932 final Layer la;
933 final ArrayList<Displayable> al;
934 DoContentChange(final Layer la) {
935 this.la = la;
936 this.al = la.getDisplayables(); // a copy
938 public Displayable getD() { return null; }
939 public boolean isEmpty() { return false; }
940 /** Check that the Displayable objects of this layer are the same and in the same quantity and order. */
941 public boolean isIdenticalTo(Object ob) {
942 if (!(ob instanceof DoContentChange)) return false;
943 final DoContentChange dad = (DoContentChange) ob;
944 if (la != dad.la || al.size() != dad.al.size()) return false;
945 // Order matters:
946 final Iterator<Displayable> it1 = al.iterator();
947 final Iterator<Displayable> it2 = dad.al.iterator();
948 for (; it1.hasNext() && it2.hasNext(); ) {
949 if (it1.next() != it2.next()) return false;
951 return true;
953 public boolean apply(final int action) {
954 // find the subset in la.al_displayables that is not in this.al
955 final HashSet<Displayable> sub1 = new HashSet<Displayable>(la.al_displayables);
956 sub1.removeAll(this.al);
957 // find the subset in this.al that is not in la.al_displayables
958 final HashSet<Displayable> sub2 = new HashSet<Displayable>(this.al);
959 sub2.removeAll(la.al_displayables);
961 HashSet<Displayable> subA=null, subB=null;
963 if (action == DoStep.UNDO) {
964 subA = sub1;
965 subB = sub2;
966 } else if (action == DoStep.REDO) {
967 subA = sub2;
968 subB = sub1;
970 if (null != subA && null != subB) {
971 // Mark Patch for mipmap file removal
972 for (final Displayable d: subA) {
973 if (d.getClass() == Patch.class) {
974 d.getProject().getLoader().queueForMipmapRemoval((Patch)d, true);
977 // ... or unmark:
978 for (final Displayable d: subB) {
979 if (d.getClass() == Patch.class) {
980 d.getProject().getLoader().queueForMipmapRemoval((Patch)d, false);
985 la.al_displayables.clear();
986 la.al_displayables.addAll(this.al);
987 la.recreateBuckets();
988 Display.updateVisibleTabs();
989 Display.clearSelection();
990 Display.update(la);
991 return true;