Updated copyright dates.
[trakem2.git] / ini / trakem2 / display / Bucket.java
blobb32aedd659ba785226381ee6d548f76fd2bc6ba2
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 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;
25 import java.util.Arrays;
26 import java.util.TreeMap;
27 import java.util.Collection;
28 import java.util.Map;
29 import java.util.ArrayList;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.Iterator;
33 import java.util.Set;
35 import java.awt.Rectangle;
36 import java.awt.geom.Area;
38 import java.awt.Graphics2D;
39 import java.awt.Color;
40 import java.awt.Stroke;
41 import java.awt.BasicStroke;
42 import java.awt.geom.AffineTransform;
44 import ini.trakem2.utils.Utils;
47 /** Each Bucket contains up to N_CHILD_BUCKETS, or a list of Displayables contained within.
49 * A Bucket is a subarea of the Layer area, which contains either other Buckets or a map of stack_index vs. Displayable instances. VERY IMPORTANT: either children is null, or map is null, but both cannot be null at the same time neither not null at the same time.
52 public class Bucket {
54 static public final int MIN_BUCKET_SIZE = 256;
56 private int bucket_side;
58 /** The sorted map of stack_index and Displayable objects that are fully contained in this bucket or intersect at top and left, but not at right and bottom. That is, the lower-right corner of the Displayable is contained with the area of this bucket. */
59 private TreeMap<Integer,Displayable> map = null;
60 /** The set of sub-buckets contained here. */
61 private ArrayList<Bucket> children = null;
63 private final int x,y,w,h;
65 private boolean empty = true;
67 public Bucket(final int x, final int y, final int w, final int h, final int bucket_side) {
68 this.x = x;
69 this.y = y;
70 this.w = w;
71 this.h = h;
72 this.bucket_side = bucket_side;
73 Utils.showStatus(new StringBuffer("Creating bucket ").append(x).append(',').append(y).append(',').append(w).append(',').append(h).toString(), false);
74 //Utils.log2(this.toString());
77 public String toString() {
78 return "Bucket: " + x + ", " + y + ", " + w + ", " + h;
81 synchronized final void populate(final Bucketable container, final HashMap<Displayable,ArrayList<Bucket>> db_map) {
82 final HashMap<Integer,Displayable> list = new HashMap<Integer,Displayable>();
83 int i = 0;
84 for (Displayable d : container.getDisplayableList()) {
85 list.put(i, d);
86 i++;
88 // cache all bounding boxes
89 final HashMap<Displayable,Rectangle> bboxes = new HashMap<Displayable,Rectangle>();
90 for (Displayable d : list.values()) {
91 bboxes.put(d, d.getBoundingBox(new Rectangle()));
93 populate(container, db_map, w+w, h+h, w, h, list, bboxes);
96 /** Recursive initialization of buckets. This method is meant to be used as init, when root is null or is made new from scratch. Returns true if not empty. */
97 final private boolean populate(final Bucketable container, final HashMap<Displayable,ArrayList<Bucket>> db_map, final int parent_w, final int parent_h, final int max_width, final int max_height, final HashMap<Integer,Displayable> parent_list, final HashMap<Displayable,Rectangle> bboxes) {
98 if (this.w <= bucket_side || this.h <= bucket_side) {
99 // add displayables, sorted by index
100 map = new TreeMap<Integer,Displayable>();
101 for (Map.Entry<Integer,Displayable> e : parent_list.entrySet()) {
102 final Displayable d = e.getValue();
103 final Rectangle bbox = bboxes.get(d);
104 if (0 == bbox.width || 0 == bbox.height) continue;
105 if (this.intersects(bbox)) {
106 map.put(e.getKey(), d);
107 putToBucketMap(d, db_map);
110 this.empty = map.isEmpty();
111 //Utils.log2(empty ? "EMPTY ": "FILLED " + this);
112 } else {
113 // create child buckets as subdivisions of this one
114 children = new ArrayList<Bucket>(2*2);
116 int side_w = (int)Math.pow(2, (int)Math.floor(Math.log(Math.max(w,h)) / Math.log(2)) - 1);
117 int side_h = side_w;
118 if (side_w > max_width) side_w = max_width;
119 if (side_h > max_height) side_h = max_height;
121 // create list of Displayables that will be added here, as extracted from the parent list
122 final HashMap<Integer,Displayable> local_list = new HashMap<Integer,Displayable>();
123 for (Map.Entry<Integer,Displayable> e : parent_list.entrySet()) {
124 final Displayable d = e.getValue();
125 final Rectangle bbox = bboxes.get(d);
126 if (0 == bbox.width || 0 == bbox.height) continue;
127 if (this.intersects(bbox)) local_list.put(e.getKey(), d);
130 //Utils.log2(local_list.size() + " :: " + this.toString());
132 for (int x=0; x<parent_w; x += side_w) {
133 if (this.x + x >= max_width) continue;
134 int width = side_w;
135 if (this.x + x + side_w > max_width) width = max_width - this.x - x;
136 for (int y=0; y<parent_h; y += side_h) {
137 if (this.y + y >= max_height) continue;
138 int height = side_h;
139 if (this.y + y + side_h > max_height) height = max_height - this.y - y;
140 final Bucket bu = new Bucket(this.x + x, this.y + y, width, height, bucket_side);
141 if (bu.populate(container, db_map, width, height, max_width, max_height, local_list, bboxes)) {
142 this.empty = false;
144 children.add(bu);
150 int w = this.w / 2;
151 int h = this.h / 2;
152 for (int i=0; i<2; i++) {
153 for (int j=0; j<2; j++) {
154 Bucket bu = new Bucket(this.x + i * w, this.y + j * h, w, h);
155 if (bu.populate(container, db_map)) {
156 //Utils.log2("FILLEd " + this);
157 this.empty = false;
158 //} else {
159 // Utils.log2("EMPTY " + this);
161 children.add(bu);
166 return !this.empty;
169 private final boolean intersects(final Rectangle r) {
170 if (r.width <= 0 || r.height <= 0 || w <= 0 || h <= 0) {
171 return false;
173 final int rw = r.x + r.width;
174 final int rh = r.y + r.height;
175 final int tw = w + x;
176 final int th = h + y;
177 // overflow || intersect
178 return ((rw < r.x || rw > x) &&
179 (rh < r.y || rh > y) &&
180 (tw < x || tw > r.x) &&
181 (th < y || th > r.y));
184 private final boolean contains(final int px, final int py) {
185 return px >= x &&
186 py >= y &&
187 px <= x + w &&
188 py <= y + h;
191 /** Find All Displayable objects that intersect with the given srcRect and return them ordered by stack_index. Of @param visible_only is true, then hidden Displayable objects are ignored. */
192 synchronized final Collection<Displayable> find(final Rectangle srcRect, final Layer layer, final boolean visible_only) {
193 final TreeMap<Integer,Displayable> accum = new TreeMap<Integer,Displayable>();
194 find(accum, srcRect, layer, visible_only);
195 return accum.values(); // sorted by integer key
198 /** Recursive search, accumulates Displayable objects that intersect the srcRect and, if @param visible_only is true, then checks first if so. */
199 private void find(final TreeMap<Integer,Displayable> accum, final Rectangle srcRect, final Layer layer, final boolean visible_only) {
200 if (empty || !intersects(srcRect)) return;
201 if (null != children) {
202 for (Bucket bu : children) {
203 bu.find(accum, srcRect, layer, visible_only);
205 } else {
206 final Rectangle tmp = new Rectangle();
207 for (Map.Entry<Integer,Displayable> entry : map.entrySet()) {
208 final Displayable d = entry.getValue();
209 if (visible_only && !d.isVisible()) continue;
210 if (d.getBoundingBox(tmp).intersects(srcRect)) {
211 accum.put(entry.getKey(), d);
217 /** Find all Displayable objects that contain the given point at the given layer (here layer acts as the Z coordinate, then) and return them ordered by stack_index. If @param visible_only is trye, then hidden Displayable objects are ignored. */
218 synchronized final Collection<Displayable> find(final int px, final int py, final Layer layer, final boolean visible_only) {
219 final TreeMap<Integer,Displayable> accum = new TreeMap<Integer,Displayable>();
220 find(accum, px, py, layer, visible_only);
221 return accum.values(); // sorted by integer key
223 /** Recursive search, accumulates Displayable objects that contain the given point and, if @param visible_only is true, then checks first if so. */
224 private void find(final TreeMap<Integer,Displayable> accum, final int px, final int py, final Layer layer, final boolean visible_only) {
225 if (empty || !contains(px, py)) return;
226 if (null != children) {
227 for (Bucket bu : children) {
228 bu.find(accum, px, py, layer, visible_only);
230 } else {
231 final Rectangle tmp = new Rectangle();
232 for (Map.Entry<Integer,Displayable> entry : map.entrySet()) {
233 final Displayable d = entry.getValue();
234 if (visible_only && !d.isVisible()) continue;
235 if (d.contains(layer, px, py)) {
236 accum.put(entry.getKey(), d);
239 //Utils.log2("Bucket with " + map.size() + " contains click " + this.toString());
243 /** Find all Displayable objects that intersect the given Area and return them ordered by stack_index. If @param visible_only is trye, then hidden Displayable objects are ignored. */
244 synchronized final Collection<Displayable> find(final Area area, final Layer layer, final boolean visible_only) {
245 final TreeMap<Integer,Displayable> accum = new TreeMap<Integer,Displayable>();
246 find(accum, area, layer, visible_only);
247 return accum.values(); // sorted by integer key
249 /** Recursive search, accumulates Displayable objects that contain the given point and, if @param visible_only is true, then checks first if so. */
250 private void find(final TreeMap<Integer,Displayable> accum, final Area area, final Layer layer, final boolean visible_only) {
251 if (empty || !intersects(area.getBounds())) return;
252 if (null != children) {
253 for (Bucket bu : children) {
254 bu.find(accum, area, layer, visible_only);
256 } else {
257 final Rectangle tmp = new Rectangle();
258 for (Map.Entry<Integer,Displayable> entry : map.entrySet()) {
259 final Displayable d = entry.getValue();
260 if (visible_only && !d.isVisible()) continue;
261 if (d.intersects(layer, area)) {
262 accum.put(entry.getKey(), d);
268 /** Update a Displayable's stack index from old to new, or a range. */
269 synchronized final void update(final Bucketable container, final Displayable d, final int old_i, final int new_i) {
270 updateRange(container, old_i, new_i);
273 final Set<Displayable> removeRange(final Bucketable container, final int first, final int last) {
274 final HashSet<Displayable> hs = new HashSet<Displayable>();
275 removeRange(container, first, last, hs);
276 return hs;
279 /** Accumulate removed Displayable instances into the HashSet. */
281 final private void removeRange(final Bucketable container, final int first, final int last, final HashSet<Displayable> hs) {
282 if (null != children) {
283 for (Bucket bu : children) bu.removeRange(container, first, last, hs);
284 } else if (null != map) {
285 // remove entire range
286 for (int i=first; i<=last; i++) {
287 final Displayable d = map.remove(i);
288 if (null != d) hs.add(d);
294 final private void updateRange(final Bucketable container, final int first, final int last) {
295 if (null != children) {
296 for (Bucket bu : children) bu.updateRange(container, first, last);
297 } else if (null != map) {
298 // remove range
299 final ArrayList<Displayable> a = new ArrayList<Displayable>();
300 for (int i=first; i<=last; i++) {
301 final Displayable d = map.remove(i);
302 if (null != d) a.add(d);
304 // re-add range with new stack_index keys
305 for (Displayable d : a) map.put(container.getDisplayableList().indexOf(d), d);
309 /** Remove from wherever it is, then test if it's in that bucket, otherwise re-add. */
310 synchronized final void updatePosition(final Displayable d, final HashMap<Displayable,ArrayList<Bucket>> db_map) {
312 final ArrayList<Bucket> list = db_map.get(d);
313 final Rectangle box = d.getBoundingBox(null);
314 final int stack_index = d.getBucketable().getDisplayableList().indexOf(d);
315 if (null != list) {
316 for (Iterator<Bucket> it = list.iterator(); it.hasNext(); ) {
317 Bucket bu = it.next();
318 if (bu.intersects(box)) continue; // no change of bucket: lower-right corner still within the bucket
319 // else, remove
320 bu.map.remove(stack_index);
321 it.remove();
324 // insert wherever appropriate, if not there
325 this.put(stack_index, d, box);
328 /** Add the given Displayable to all buckets that intercept its bounding box. */
329 synchronized final void put(final int stack_index, final Displayable d, final Rectangle box) {
330 if (!intersects(box)) return;
331 // there will be at least one now
332 this.empty = false;
333 if (null != children) {
334 for (Bucket bu : children) bu.put(stack_index, d, box);
335 } else if (null != map) {
336 map.put(stack_index, d);
337 putToBucketMap(d, d.getBucketable().getBucketMap()); // the db_map
340 private void debugMap(String title) {
341 if (null == map) return;
342 Utils.log2("@@@ " + title);
343 for (Map.Entry<Integer,Displayable> e: map.entrySet()) {
344 Utils.log2("k,v : " + e.getKey() + " , " + e.getValue());
348 final private void putToBucketMap(final Displayable d, final HashMap<Displayable,ArrayList<Bucket>> db_map) {
349 ArrayList<Bucket> list = db_map.get(d);
350 if (null == list) {
351 list = new ArrayList<Bucket>();
352 db_map.put(d, list);
353 list.add(this);
354 } else if (!list.contains(this)) list.add(this);
357 final private void removeFromBucketMap(final Displayable d, final HashMap<Displayable,ArrayList<Bucket>> db_map) {
358 ArrayList<Bucket> list = db_map.get(d);
359 if (null == list) return;
360 list.remove(d);
361 if (0 == list.size()) db_map.remove(d);
365 /** Returns whether this bucket is empty of Displayable objects, and accumulates removed Displayable in the set. */
366 final private boolean remove(final int stack_index, final Set<Displayable> hs) {
367 if (null != children) {
368 this.empty = true;
369 for (Bucket bu : children) {
370 if (!bu.remove(stack_index, hs)) this.empty = false;
372 return this.empty;
373 } else if (null != map) {
374 final Displayable d = map.remove(stack_index);
375 if (null != d) hs.add(d);
376 else Utils.log2("Bucket could not remove Displayable at stack index " + stack_index);
377 return map.isEmpty();
379 return true;
382 synchronized final Set<Displayable> remove(final int stack_index) {
383 final HashSet<Displayable> hs = new HashSet<Displayable>();
384 remove(stack_index, hs);
385 return hs;
388 static final void remove(final Displayable d, final HashMap<Displayable, ArrayList<Bucket>> db_map) {
389 final int stack_index = d.getBucketable().getDisplayableList().indexOf(d);
390 final ArrayList<Bucket> list = db_map.get(d);
391 if (null == list) return;
392 for (Bucket bu : list) {
393 bu.remove(stack_index);
395 db_map.remove(d);
398 synchronized public void paint(Graphics2D g, Rectangle srcRect, double mag, Color color) {
399 if (null == map) {
400 for (Bucket bu : children) bu.paint(g, srcRect, mag, color);
401 return;
403 if (!intersects(srcRect)) return;
404 final Graphics2D g2d = (Graphics2D)g;
405 final Stroke original_stroke = g2d.getStroke();
406 AffineTransform original = g2d.getTransform();
407 g2d.setTransform(new AffineTransform());
408 g2d.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
409 g.setColor(color);
410 g.drawRect((int)((x - srcRect.x) * mag), (int)((y-srcRect.y)*mag), (int)(w*mag), (int)(h*mag));
411 g2d.setStroke(original_stroke);
412 g2d.setTransform(original);
415 /** Determine whether the rectangle is smaller than the layer dimensions padded in by one bucket_side -- if not, makes little sense to use buckets, and it's better to do linear search without the TreeMap overhead. */
416 public final boolean isBetter(final Rectangle r, final Bucketable container) {
418 final boolean b = r.width * r.height < (layer.getLayerWidth() - bucket_side) * (layer.getLayerHeight() - bucket_side);
419 Utils.log2("isBetter: " + b);
420 if (b) {
421 Utils.log2("\t r is " + r.width + ", " + r.height);
422 Utils.log2("\t o is " + (int)(layer.getLayerWidth() - bucket_side) + ", " + (int)(layer.getLayerHeight() * bucket_side));
424 return b;
426 return r.width * r.height < (container.getLayerWidth() - bucket_side) * (container.getLayerHeight() - bucket_side);
429 private final ArrayList<Bucket>getChildren(final ArrayList<Bucket> bus) {
430 if (null != children) {
431 for (Bucket bu : children) {
432 bu.getChildren(bus);
434 } else if (null != map) {
435 bus.add(this);
437 return bus;
440 public void debug() {
441 Utils.log2("total map buckets: " + getChildren(new ArrayList<Bucket>()).size());
444 static public int getBucketSide(final Bucketable container) {
445 if (null != container.getProject().getProperty("bucket_side")) {
446 return (int)container.getProject().getProperty("bucket_side", Bucket.MIN_BUCKET_SIZE);
447 } else {
448 // estimate median
449 final ArrayList<Displayable> col = (ArrayList<Displayable>)container.getDisplayableList();
450 if (0 == col.size()) return 2048;
451 final int[] sizes = new int[col.size()];
452 final Rectangle r = new Rectangle();
453 int i = 0;
454 for (Displayable d : col) {
455 d.getBoundingBox(r);
456 sizes[i++] = Math.max(r.width, r.height);
458 Arrays.sort(sizes);
459 int size = 2 * sizes[sizes.length/2];
460 return size > 128 ? size : 128;