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.
23 package ini
.trakem2
.display
;
25 import java
.util
.Arrays
;
26 import java
.util
.TreeMap
;
27 import java
.util
.Collection
;
29 import java
.util
.ArrayList
;
30 import java
.util
.HashMap
;
31 import java
.util
.HashSet
;
32 import java
.util
.Iterator
;
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.
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
) {
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
>();
84 for (Displayable d
: container
.getDisplayableList()) {
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);
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);
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;
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;
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
)) {
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);
159 // Utils.log2("EMPTY " + this);
169 private final boolean intersects(final Rectangle r
) {
170 if (r
.width
<= 0 || r
.height
<= 0 || w
<= 0 || h
<= 0) {
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
) {
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
);
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
);
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
);
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);
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
) {
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
);
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
320 bu
.map
.remove(stack_index
);
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
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
);
351 list
= new ArrayList
<Bucket
>();
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;
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
) {
369 for (Bucket bu
: children
) {
370 if (!bu
.remove(stack_index
, hs
)) this.empty
= false;
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();
382 synchronized final Set
<Displayable
> remove(final int stack_index
) {
383 final HashSet
<Displayable
> hs
= new HashSet
<Displayable
>();
384 remove(stack_index
, 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
);
398 synchronized public void paint(Graphics2D g
, Rectangle srcRect
, double mag
, Color color
) {
400 for (Bucket bu
: children
) bu
.paint(g
, srcRect
, mag
, color
);
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
));
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);
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));
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
) {
434 } else if (null != map
) {
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
);
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();
454 for (Displayable d
: col
) {
456 sizes
[i
++] = Math
.max(r
.width
, r
.height
);
459 int size
= 2 * sizes
[sizes
.length
/2];
460 return size
> 128 ? size
: 128;