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.
23 package ini
.trakem2
.display
;
26 import java
.awt
.event
.*;
28 import java
.awt
.geom
.AffineTransform
;
29 import java
.awt
.geom
.Area
;
30 import java
.awt
.geom
.NoninvertibleTransformException
;
31 import java
.awt
.geom
.Point2D
;
32 import ij
.gui
.GenericDialog
;
33 import ij
.measure
.ResultsTable
;
34 import ini
.trakem2
.Project
;
35 import ini
.trakem2
.persistence
.DBObject
;
36 import ini
.trakem2
.utils
.IJError
;
37 import ini
.trakem2
.utils
.Utils
;
38 import ini
.trakem2
.utils
.M
;
39 import ini
.trakem2
.utils
.Search
;
40 import ini
.trakem2
.vector
.Compare
;
42 /** The class that any element to be drawn on a Display must extend. */
43 public abstract class Displayable
extends DBObject
{
45 final protected AffineTransform at
= new AffineTransform();
47 /** Width and height of the data, not the bounding box. If the AffineTransform is different than identity, then the bounding box will be different. */
48 protected double width
= 0,
51 protected boolean locked
= false;
52 protected String title
;
53 protected Color color
= Color
.yellow
;
54 protected float alpha
= 1.0f
; // from 0 to 1 (0 is full transparency)
55 protected boolean visible
= true;
56 protected Layer layer
;
57 /** The Displayable objects this one is linked to. Can be null. */
58 protected HashSet
<Displayable
> hs_linked
= null;
60 /** The table of key/value property pairs of this Displayable. */
61 protected Map
<String
,String
> props
= null;
63 /** The table of tables of properties: for any target Displayable, there is a set of properties specific of the relationship between this Displayable and the target one. */
64 protected Map
<Displayable
,Map
<String
,String
>> linked_props
= null;
66 /** The call back hooks to remove any linked properties in other Displayable instances when this Displayable is removed. */
67 protected Set
<Displayable
> linked_props_origins
= null;
69 /** Set a key/valye property pair; to remove a property, set the value to null. */
70 synchronized public boolean setProperty(final String key
, final String value
) {
71 if (null == key
) return false;
72 if (null != props
&& null == value
) {
74 if (props
.isEmpty()) props
= null;
76 if (null == props
) props
= new HashMap
<String
,String
>();
77 props
.put(key
, value
);
82 /** If key is null or not found, returns null; otherwise returns the stored value for key. */
83 public String
getProperty(final String key
) {
84 return getProperty(key
, null);
87 /** If key is null or not found, returns default_value; otherwise returns the stored value for key. */
88 synchronized public String
getProperty(final String key
, final String default_value
) {
89 if (null == key
|| null == props
) return default_value
;
90 final String val
= props
.get(key
);
91 if (null == val
) return default_value
;
95 /** Returns a copy of this object's properties, or null if none. */
96 synchronized public Map
getProperties() {
97 if (null == props
) return null;
98 return new HashMap(props
);
101 /** Add a property that is specific to the relationship between this Displayable and the target, and will be deleted when the target Displayable is deleted. */
102 synchronized public boolean setLinkedProperty(final Displayable target
, final String key
, final String value
) {
103 if (null == target
|| null == key
) return false;
104 if (target
.project
!= this.project
) {
105 Utils
.log("Cannot link to a Displayable from another project!");
108 if (null != linked_props
&& null == value
) {
109 // Remove the key and cleanup if necessary
110 Map
<String
,String
> p
= linked_props
.get(target
);
114 linked_props
.remove(target
);
115 if (linked_props
.isEmpty()) {
121 linkedProps(target
).put(key
, value
);
126 /** Obtain the existing or a new, if null, map of linked properties with target. Also sets the remove callback hook to remove the map of linked properties when the target is removed. */
127 private final Map
<String
,String
> linkedProps(final Displayable target
) {
128 if (null == linked_props
) linked_props
= new HashMap
<Displayable
,Map
<String
,String
>>();
129 Map
<String
,String
> p
= linked_props
.get(target
);
131 p
= new HashMap
<String
,String
>();
132 linked_props
.put(target
, p
);
134 // Add hook on the target side: (so when the target is deleted, it will call removeLinkedProperties here)
135 if (null == target
.linked_props_origins
) {
136 target
.linked_props_origins
= new HashSet
<Displayable
>();
138 target
.linked_props_origins
.add(this);
143 /** Add all the properties in map p as properties linked to the target Displayable. If any keys already existed their values will be overwritten by those in p. Any keys whose value is null will be removed. */
144 synchronized public void setLinkedProperties(final Displayable target
, final Map
<String
,String
> p
) {
145 if (null == target
|| null == p
) return;
146 if (target
.project
!= this.project
) {
147 Utils
.log("Cannot link to a Displayable from another project!");
150 final Map
<String
,String
> lp
= linkedProps(target
);
151 for (final Map
.Entry
<String
,String
> e
: p
.entrySet()) {
152 final String value
= e
.getValue();
154 // Remove key from table
155 lp
.remove(e
.getKey());
157 lp
.put(e
.getKey(), value
);
162 linked_props
.remove(target
);
166 /** Removes and returns the table of properties lined to the target Displayable. May be empty. */
167 synchronized public Map
<String
,String
> removeLinkedProperties(final Displayable target
) {
168 if (null == target
|| null == linked_props
) return new HashMap
<String
,String
>();
169 final Map
<String
,String
> p
= linked_props
.remove(target
);
170 if (linked_props
.isEmpty()) {
173 if (null == p
) return new HashMap
<String
,String
>();
177 /** Tell any other Displayable that has a linked property with this Displayable to remove it. */
178 protected void removeLinkedPropertiesFromOrigins() {
179 if (null != this.linked_props_origins
) {
180 for (final Displayable origin
: linked_props_origins
) {
181 origin
.removeLinkedProperties(this);
186 /** If the key is null or not found, or the aren't any properties linked to the target, returns null; otherwise returns the stored value for key and target. */
187 public String
getLinkedProperty(final Displayable target
, final String key
) {
188 return getLinkedProperty(target
, key
, null);
191 /** If the key is null or not found, or the aren't any properties linked to the target, returns default_value; otherwise returns the stored value for key and target. */
192 synchronized public String
getLinkedProperty(final Displayable target
, final String key
, final String default_value
) {
193 if (null == target
|| null == key
) return default_value
;
194 if (target
.project
!= this.project
) {
195 Utils
.log("You attempted to get a property for a Displayable of another project, which is impossible.");
196 return default_value
;
198 if (null == linked_props
) return default_value
;
199 final Map
<String
,String
> p
= linked_props
.get(target
);
200 if (null == p
) return default_value
;
201 final String value
= p
.get(key
);
202 if (null == value
) return default_value
;
206 /** Returns a copy of this object's linked properties table for the given Displayable, which may be empty. */
207 synchronized public Map
<String
,String
> getLinkedProperties(final Displayable target
) {
208 if (null == target
|| null == linked_props
) return null;
209 final Map
<String
,String
> m
= linked_props
.get(target
);
210 if (null == m
) return new HashMap
<String
,String
>();
211 return new HashMap
<String
,String
>(m
);
214 /** Returns a copy of this object's linked properties, which may be empty. */
215 synchronized public Map
<Displayable
,Map
<String
,String
>> getLinkedProperties() {
216 final Map
<Displayable
,Map
<String
,String
>> lp
= new HashMap
<Displayable
,Map
<String
,String
>>();
217 if (null == linked_props
) return lp
;
218 for (final Map
.Entry
<Displayable
,Map
<String
,String
>> e
: linked_props
.entrySet()) {
219 lp
.put(e
.getKey(), new HashMap
<String
,String
>(e
.getValue()));
224 ////////////////////////////////////////////////////
225 public void setLocked(boolean lock
) {
226 if (lock
) this.locked
= lock
;
228 // to unlock, unlock those in the linked group that are locked
230 unlockAllLinked(new HashSet());
232 updateInDatabase("locked");
234 private void unlockAllLinked(HashSet hs
) {
235 if (hs
.contains(this)) return;
237 if (null == hs_linked
) return;
238 for (final Displayable d
: hs_linked
) {
239 if (d
.locked
) d
.locked
= false;
240 d
.unlockAllLinked(hs
);
243 /** Return the value of the field 'locked'. */
244 public boolean isLocked2() {
247 /** Check if this or any of the Displayables in the linked group is locked. */
248 public boolean isLocked() {
249 if (locked
) return true;
250 return isLocked(new HashSet());
252 private boolean isLocked(HashSet hs
) {
253 if (locked
) return true;
254 else if (hs
.contains(this)) return false;
256 if (null != hs_linked
&& hs_linked
.size() > 0) {
257 for (final Displayable d
: hs_linked
) {
258 if (d
.isLocked(hs
)) return true;
263 ////////////////////////////////////////////////////
264 /** The minimal public Displayable constructor. */
265 public Displayable(Project project
, String title
, double x
, double y
) {
268 this.at
.translate(x
, y
);
271 /** Reconstruct a Displayable from the database. */
272 public Displayable(Project project
, long id
, String title
, boolean locked
, AffineTransform at
, double width
, double height
) {
275 this.locked
= locked
;
276 if (null != at
) this.at
.setTransform(at
);
278 this.height
= height
;
281 /** Reconstruct a Displayable from an XML entry. Used entries get removed from the HashMap. */
282 public Displayable(Project project
, long id
, HashMap ht
, HashMap ht_links
) {
284 double x
=0, y
=0, rot
=0; // for backward compatibility
285 this.layer
= null; // will be set later
286 // parse data // TODO this is weird, why not just call them, since no default values are set anyway
287 final ArrayList al_used_keys
= new ArrayList();
288 for (Iterator it
= ht
.entrySet().iterator(); it
.hasNext(); ) {
289 Map
.Entry entry
= (Map
.Entry
)it
.next();
290 String key
= (String
)entry
.getKey();
291 String data
= (String
)entry
.getValue();
293 if (key
.equals("width")) width
= Double
.parseDouble(data
);
294 else if (key
.equals("height")) height
= Double
.parseDouble(data
);
295 else if (key
.equals("transform")) {
296 final String
[] nums
= data
.substring(data
.indexOf('(')+1, data
.lastIndexOf(')')).split(",");
297 this.at
.setTransform(Double
.parseDouble(nums
[0]), Double
.parseDouble(nums
[1]),
298 Double
.parseDouble(nums
[2]), Double
.parseDouble(nums
[3]),
299 Double
.parseDouble(nums
[4]), Double
.parseDouble(nums
[5]));
300 //Utils.log2("at: " + this.at);
302 else if (key
.equals("locked")) locked
= data
.trim().toLowerCase().equals("true");
303 else if (key
.equals("visible")) visible
= data
.trim().toLowerCase().equals("true");
304 else if (key
.equals("style")) {
307 int i_start
= data
.indexOf("stroke-opacity:");
310 i_start
= data
.indexOf("fill-opacity:");
315 this.alpha
= Float
.parseFloat(data
.substring(i_start
+ 13, data
.indexOf(';', i_start
+ 13)).trim());
318 this.alpha
= Float
.parseFloat(data
.substring(i_start
+ 15, data
.indexOf(';', i_start
+ 15)).trim());
321 i_start
= data
.indexOf("stroke:");
325 i_start
= data
.indexOf("fill:");
328 i_start
= data
.indexOf('#', data
.indexOf(':', i_start
+4)); // at least 4 (for 'fill:'), max 7 (for 'stroke:')
329 i_end
= data
.indexOf(';', i_start
+1);
330 this.color
= Utils
.getRGBColorFromHex(data
.substring(i_start
+1, i_end
));
332 Utils
.log2("Can't parse color for id=" + id
);
333 this.color
= Color
.yellow
;
335 // avoid recording this key for deletion, for the DLabel to read it
337 } catch (Exception es
) {
338 if (null == this.color
) this.color
= Color
.yellow
;
339 Utils
.log("ERROR at reading style: " + es
);
341 } else if (key
.equals("links")) {
342 // This is hard one, must be stored until all objects exist and then processed
343 if (null != data
&& data
.length() > 0) ht_links
.put(this, data
);
344 } else if (key
.equals("title")) {
345 if (null != data
&& !data
.toLowerCase().equals("null")) {
346 this.title
= data
.replaceAll("^#^", "\""); // fix " and backslash characters
347 } else this.title
= null;
348 } else if (key
.equals("x")) {
349 x
= Double
.parseDouble(data
); // this could be done with reflection, but not all, hence this dullness
350 } else if (key
.equals("y")) {
351 y
= Double
.parseDouble(data
);
352 } else if (key
.equals("rot")) {
353 rot
= Double
.parseDouble(data
);
355 al_used_keys
.add(key
);
356 } catch (Exception ea
) {
357 Utils
.log(this + " : failed to read data for key '" + key
+ "':\n" + ea
);
360 for (Iterator it
= al_used_keys
.iterator(); it
.hasNext(); ) {
361 ht
.remove(it
.next());
364 // support old versions:
365 if (this.at
.isIdentity() && (0 != x
|| 0 != y
|| 0 != rot
)) {
366 this.at
.translate(x
, y
);
368 AffineTransform at2
= new AffineTransform();
369 at2
.rotate(Math
.toRadians(rot
), x
+ width
/2, y
+ height
/2);
370 this.at
.preConcatenate(at2
);
373 // scaling in old versions will be lost
376 public void paint(Graphics2D g
, double magnification
, boolean active
, int channels
, Layer active_layer
) {
377 Utils
.log2("paint g, magnification, active, channels, active_layer: not implemented yet for " + this.getClass());
380 /** If the painting is expensive, children classes can override this method to provide first a coarse painting, and then call repaint on their own again once the desired graphics are ready. */
381 public void prePaint(Graphics2D g
, double magnification
, boolean active
, int channels
, Layer active_layer
) {
382 paint(g
, magnification
, active
, channels
, active_layer
);
385 /** Not accepted if zero or negative. Remakes the snapshot, updates the snapshot panel and the Display. */
386 public void setDimensions(double width
, double height
) {
387 setDimensions(width
, height
, true);
390 /** Sets the dimensions of the bounding box. Not accepted if zero or negative. If repaint is true, remakes the snapshot, updates the snapshot panel and the Display. */
391 public void setDimensions(double width
, double height
, boolean repaint
) {
392 if (width
<= 0 || height
<= 0) return;
393 Rectangle b
= getBoundingBox(null);
394 if (b
.width
== width
&& b
.height
== height
) return;
395 double sx
= width
/ (double)b
.width
;
396 double sy
= height
/ (double)b
.height
;
397 this.scale(sx
, sy
, b
.x
, b
.y
); // relative to top left corner
399 Display
.repaint(layer
, this, 5);
400 //done with above//Display.updatePanel(layer, this);
404 public void setLayer(Layer layer
, boolean update_db
) {
405 if (null == layer
|| this.layer
== layer
) return;
407 if (update_db
) updateInDatabase("layer_id");
410 public void setLayer(Layer layer
) {
411 setLayer(layer
, true);
414 public Layer
getLayer() {
418 /** Does not accept null or zero-length titles. */
419 public void setTitle(String title
) {
420 if (null == title
|| 0 == title
.length()) return;
422 Display
.updateTitle(layer
, this); // update the DisplayablePanel(s) that show this Patch
423 updateInDatabase("title");
426 public String
getTitle() {
430 public String
getShortTitle() {
431 Rectangle b
= getBoundingBox(null);
432 return "x=" + Utils
.cutNumber(b
.x
, 2) + " y=" + Utils
.cutNumber(b
.y
, 2) + (null != layer ?
" z=" + Utils
.cutNumber(layer
.getZ(), 2) : "");
435 /** Returns the x of the bounding box. */
436 public double getX() {
437 return getBoundingBox(null).x
;
440 /** Returns the y of the bounding box. */
441 public double getY() {
442 return getBoundingBox(null).y
;
445 /** Returns the width of the data. */
446 public double getWidth() {
450 /** Returns the height of the data. */
451 public double getHeight() {
455 /** Bounding box of the transformed data. */
456 public Rectangle
getBoundingBox() {
457 return getBoundingBox(null);
460 /** Will fill bounding box values into given rectangle -- only that part of this object showing in the given layer will be included in the box. */
461 public Rectangle
getBounds(final Rectangle r
, final Layer layer
) {
462 return getBoundingBox(r
);
465 /** Bounding box of the transformed data. Saves one Rectangle allocation, returns the same Rectangle, modified (or a new one if null). */
466 public Rectangle
getBoundingBox(final Rectangle r
) {
467 return getBounds(null != r ? r
: new Rectangle());
470 /** Bounding box of the transformed data. Saves one allocation, returns the same Rectangle, modified (or a new one if null). */
471 private final Rectangle
getBounds(final Rectangle r
) {
474 r
.width
= (int)this.width
;
475 r
.height
= (int)this.height
;
476 if (this.at
.getType() == AffineTransform
.TYPE_TRANSLATION
) {
477 r
.x
+= (int)this.at
.getTranslateX();
478 r
.y
+= (int)this.at
.getTranslateY();
480 //r = transformRectangle(r);
482 final double[] d1
= new double[]{0, 0, width
, 0, width
, height
, 0, height
};
483 final double[] d2
= new double[8];
484 this.at
.transform(d1
, 0, d2
, 0, 4);
486 double min_x
=Double
.MAX_VALUE
, min_y
=Double
.MAX_VALUE
, max_x
=-min_x
, max_y
=-min_y
;
487 for (int i
=0; i
<d2
.length
; i
+=2) {
488 if (d2
[i
] < min_x
) min_x
= d2
[i
];
489 if (d2
[i
] > max_x
) max_x
= d2
[i
];
490 if (d2
[i
+1] < min_y
) min_y
= d2
[i
+1];
491 if (d2
[i
+1] > max_y
) max_y
= d2
[i
+1];
495 r
.width
= (int)(max_x
- min_x
);
496 r
.height
= (int)(max_y
- min_y
);
501 /** Subclasses can override this method to provide the exact contour, otherwise it returns the transformed bounding box of the data. */
502 public Polygon
getPerimeter() {
503 if (this.at
.isIdentity() || this.at
.getType() == AffineTransform
.TYPE_TRANSLATION
) {
504 // return the bounding box as a polygon:
505 final Rectangle r
= getBoundingBox();
506 return new Polygon(new int[]{r
.x
, r
.x
+r
.width
, r
.x
+r
.width
, r
.x
},
507 new int[]{r
.y
, r
.y
, r
.y
+r
.height
, r
.y
+r
.height
},
510 // else, the rotated/sheared/scaled and translated bounding box:
511 final double[] po1
= new double[]{0,0, width
,0, width
,height
, 0,height
};
512 final double[] po2
= new double[8];
513 this.at
.transform(po1
, 0, po2
, 0, 4);
514 return new Polygon(new int[]{(int)po2
[0], (int)po2
[2], (int)po2
[4], (int)po2
[6]},
515 new int[]{(int)po2
[1], (int)po2
[3], (int)po2
[5], (int)po2
[7]},
519 /** Returns the perimeter enlarged in all West, North, East and South directions, in pixels.*/
520 public Polygon
getPerimeter(final int w
, final int n
, final int e
, final int s
) {
521 if (this.at
.isIdentity() || this.at
.getType() == AffineTransform
.TYPE_TRANSLATION
) {
522 // return the bounding box as a polygon:
523 final Rectangle r
= getBoundingBox();
524 return new Polygon(new int[]{r
.x
-w
, r
.x
+r
.width
+w
+e
, r
.x
+r
.width
+w
+e
, r
.x
-w
},
525 new int[]{r
.y
-n
, r
.y
-n
, r
.y
+r
.height
+n
+s
, r
.y
+r
.height
+n
+s
},
528 // else, the rotated/sheared/scaled and translated bounding box:
529 final double[] po1
= new double[]{-w
,-n
, width
+w
+e
,-n
, width
+w
+e
,height
+n
+s
, -w
,height
+n
+s
};
530 final double[] po2
= new double[8];
531 this.at
.transform(po1
, 0, po2
, 0, 4);
532 return new Polygon(new int[]{(int)po2
[0], (int)po2
[2], (int)po2
[4], (int)po2
[6]},
533 new int[]{(int)po2
[1], (int)po2
[3], (int)po2
[5], (int)po2
[7]},
537 /** Test whether the given point falls within the perimeter of this Displayable, considering the position x,y. Used by the DisplayCanvas mouse events. */
538 public boolean contains(final int x_p
, final int y_p
) {
539 return getPerimeter().contains(x_p
, y_p
);
542 /** Calls contains(x_p, y_p) unless overriden -- in ZDisplayable objects, it tests whether the given point is contained in the part of the ZDisplayable that shows in the given layer. */
543 public boolean contains(final Layer layer
, final int x_p
, final int y_p
) {
544 return contains(x_p
, y_p
);
547 public void setAlpha(float alpha
) {
548 setAlpha(alpha
, true);
551 protected void setAlpha(float alpha
, boolean update
) {
552 if (alpha
!= this.alpha
&& alpha
>= 0.0f
&& alpha
<= 1.0f
) {
555 Display
.repaint(layer
, this, 5);
556 updateInDatabase("alpha");
557 Display3D
.setTransparency(this, alpha
);
562 public float getAlpha() { return alpha
; }
564 public Color
getColor() { return color
; }
566 /** Return the HashSet of directly linked Displayable objects. */
567 public HashSet
<Displayable
> getLinked() { return hs_linked
; }
569 /** Return those of Class c from among the directly linked. */
570 public HashSet
<Displayable
> getLinked(final Class c
) {
571 if (null == hs_linked
) return null;
572 final HashSet
<Displayable
> hs
= new HashSet
<Displayable
>();
573 for (final Displayable d
: hs_linked
) {
574 if (d
.getClass() == c
) hs
.add(d
);
579 /** Return the HashSet of all directly and indirectly linked objects. */
580 public HashSet
<Displayable
> getLinkedGroup(HashSet
<Displayable
> hs
) {
581 if (null == hs
) hs
= new HashSet
<Displayable
>();
582 else if (hs
.contains(this)) return hs
;
584 if (null == hs_linked
) return hs
;
585 for (final Displayable d
: hs_linked
) {
586 d
.getLinkedGroup(hs
);
591 public void mousePressed(MouseEvent me
, int x_p
, int y_p
, double mag
) {
592 Utils
.log2("mousePressed not implemented yet for " + this.getClass().getName());
594 public void mouseDragged(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_d_old
, int y_d_old
) {
595 Utils
.log2("mouseDragged not implemented yet for " + this.getClass().getName());
597 public void mouseReleased(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_r
, int y_r
) {
598 Utils
.log2("mouseReleased not implemented yet for " + this.getClass().getName());
601 public void keyPressed(KeyEvent ke
) {
603 int key_code
= ke
.getKeyCode();
607 case KeyEvent
.VK_ENTER
: //End transform or bring ImageJ to front
610 case KeyEvent
.VK_UP
: //move one pixel up
611 translate(0, -1, true);
614 case KeyEvent
.VK_DOWN
: //move one pixel down
615 translate(0, 1, true);
618 case KeyEvent
.VK_LEFT
: //move one pixel left
619 translate(-1, 0, true);
622 case KeyEvent
.VK_RIGHT
: //move one pixel right
623 translate(1, 0, true);
626 case KeyEvent
.VK_ESCAPE
:
627 Display
.setActive(ke
, null);
632 Rectangle box
= getLinkedBox(true);
634 if (ke
.isConsumed() && KeyEvent
.VK_ESCAPE
!= key_code
) {
635 // no need to repaint the previous box, falls within 1 pixel (and here I add 5 on the margins)
636 Display
.repaint(this.layer
, box
, 5);
637 } //not repainting for ESC because it has been done already
640 public boolean isVisible() { return this.visible
; }
642 public final void setVisible(boolean visible
) {
643 setVisible(visible
, true);
646 public void setVisible(final boolean visible
, final boolean repaint
) {
647 if (visible
== this.visible
) {
648 // patching synch error
649 Display
.updateVisibilityCheckbox(layer
, this, null);
652 this.visible
= visible
;
654 //Display.setUpdateGraphics(layer, this);
655 Display
.repaint(layer
, this, 5);
657 updateInDatabase("visible");
658 Display
.updateVisibilityCheckbox(layer
, this, null);
661 /** Repaint this Displayable in all Display instances that are showing it. */
662 public void repaint() {
663 Display
.repaint(layer
, this, 5);
666 public void setColor(Color color
) {
667 if (null == color
|| color
.equals(this.color
)) return;
669 updateInDatabase("color");
670 Display
.repaint(layer
, this, 5);
671 Display3D
.setColor(this, color
);
674 /** Release resources. */
675 public void destroy() {
679 public boolean isOutOfRepaintingClip(final double magnification
, final Rectangle srcRect
, final Rectangle clipRect
) {
680 // 1 - check visibility
682 //if not visible, it's out, so return true:
685 final Rectangle box
= getBoundingBox(null); // includes rotation
686 // 2 - check if out of clipRect (clipRect is in screen coords, whereas srcRect is in offscreen coords)
687 if (null != clipRect
&& null != srcRect
) {
688 final int screen_x
= (int)((box
.x
-srcRect
.x
) * magnification
);
689 final int screen_y
= (int)((box
.y
-srcRect
.y
) * magnification
);
690 final int screen_width
= (int)(box
.width
* magnification
);
691 final int screen_height
= (int)(box
.height
* magnification
);
692 if ((screen_x
+ screen_width
) < clipRect
.x
|| (screen_y
+ screen_height
) < clipRect
.y
|| screen_x
> (clipRect
.x
+ clipRect
.width
) || screen_y
> (clipRect
.y
+ clipRect
.height
)) {
697 // 3 - check if out of srcRect
698 // This is included above anyway, but just in case there is no clipRect !
699 if (null != srcRect
) {
700 if (box
.x
+ box
.width
< srcRect
.x
|| box
.y
+ box
.height
< srcRect
.y
|| box
.x
> srcRect
.x
+ srcRect
.width
|| box
.y
> srcRect
.y
+ srcRect
.height
) {
705 // else: it's in, so paint!
709 /** For the DisplayNavigator. No srcRect or magnification considered. */
710 public boolean isOutOfRepaintingClip(Rectangle clipRect
, double scale
) {
711 // 1 - check visibility
713 //if not visible, it's out, so return true:
716 Rectangle box
= getBoundingBox(); // includes rotation
717 // 2 - check if out of clipRect
718 if (null != clipRect
) {
719 int screen_x
= (int)(box
.x
* scale
);
720 int screen_y
= (int)(box
.y
* scale
);
721 int screen_width
= (int)(box
.width
* scale
);
722 int screen_height
= (int)(box
.height
* scale
);
723 if ((screen_x
+ screen_width
) < clipRect
.x
|| (screen_y
+ screen_height
) < clipRect
.y
|| screen_x
> (clipRect
.x
+ clipRect
.width
) || screen_y
> (clipRect
.y
+ clipRect
.height
)) {
728 // else: it's in, so paint!
732 /** Remove also from the trees if present; does nothing more than remove(boolean) unless overriden. */
733 protected boolean remove2(final boolean check
) {
734 return remove(check
);
737 /** Remove from both the database and any Display that shows the Layer in which this Displayable is shown. */
738 public boolean remove(final boolean check
) {
739 if (super.remove(check
) && layer
.remove(this)) { // TODO there should be Displayable2D and Displayable3D, and each extend Displayable
741 removeLinkedPropertiesFromOrigins();
743 Compare
.remove(this);
747 Utils
.log("Failed to remove " + this.getClass().getName() + " " + this);
749 // WARNING -- the ZDisplayable.remove should take ALSO any changes I add here later
752 /** Link the given Displayable with this Displayable, and then tell the given Displayable to link this. Since the link is stored as Displayable objects in a HashSet, there'll never be repeated entries. */
753 public void link(final Displayable d
) {
757 /** Link the given Displayable with this Displayable, and then tell the given Displayable to link this. Since the link is stored as Displayable objects in a HashSet, there'll never be repeated entries.*/
758 public void link(final Displayable d
, final boolean update_database
) { // the boolean is used by the loader when reconstructing links.
759 if (this == d
) return;
760 if (null == this.hs_linked
) this.hs_linked
= new HashSet
<Displayable
>();
761 // link the other to this
762 this.hs_linked
.add(d
);
763 // link this to the other
764 if (null == d
.hs_linked
) d
.hs_linked
= new HashSet();
765 d
.hs_linked
.add(this);
766 // update the database
767 if (update_database
) project
.getLoader().addCrossLink(project
.getId(), this.id
, d
.id
);
770 /** Remove all links held by this Displayable.*/
771 public void unlink() {
772 if (null == this.hs_linked
) return;
773 final Displayable
[] displ
= new Displayable
[hs_linked
.size()];
774 hs_linked
.toArray(displ
);
776 // all these redundancy because of the [typical] 'concurrent modification exception'
777 for (int i
=0; i
<displ
.length
; i
++) {
780 this.hs_linked
= null;
783 /** Remove the link with the given Displayable, and tell the given Displayable to remove the link with this. */
784 public void unlink(final Displayable d
) {
785 //Utils.log("Displayable.unlink(Displayable)");
787 return; // should not happen
789 if (null == this.hs_linked
) return; // should not happen
790 // unlink the other from this, and this from the other
791 if (!( hs_linked
.remove(d
) && d
.hs_linked
.remove(this))) {
792 // signal database inconsistency (should not happen)
793 Utils
.log("Database inconsistency: two displayables had a non-reciprocal link. BEWARE of other errors.");
795 // update the database in any case
796 project
.getLoader().removeCrossLink(this.id
, d
.id
);
799 /** Check if this object is directly linked to any other Displayable objects.*/
800 public boolean isLinked() {
801 if (null == hs_linked
) return false;
802 return !hs_linked
.isEmpty();
805 /** Check if this object is directly linked to a Displayable object of the given Class. */
806 public boolean isLinked(final Class c
) {
807 if (null == hs_linked
) return false;
808 for (final Displayable d
: hs_linked
) {
809 if (c
.isInstance(d
)) return true;
814 /** Check if thisobject is directly linked to the given Displayable. */
815 public boolean isLinked(final Displayable d
) {
816 if (null == hs_linked
) return false;
817 return hs_linked
.contains(d
);
820 /** Check if this object is directly linked only to Displayable objects of the given class (returns true) or to none (returns true as well).*/
821 public boolean isOnlyLinkedTo(final Class c
) {
822 if (null == hs_linked
|| hs_linked
.isEmpty()) return true;
823 for (final Displayable d
: hs_linked
) {
824 if (d
.getClass() != c
) return false;
829 /** Check if this object is directly linked only to Displayable objects of the given class in the same layer (returns true). Returns true as well when not linked to any of the given class.*/
830 public boolean isOnlyLinkedTo(final Class c
, final Layer layer
) {
831 if (null == hs_linked
|| hs_linked
.isEmpty()) return true;
832 for (final Displayable d
: hs_linked
) {
833 // if the class is not the asked one, or the object is not in the same layer, return false!
834 if (d
.getClass() != c
|| d
.layer
!= this.layer
) return false;
840 /** Link the Patch objects that lay underneath the bounding box of this Displayable, so that they cannot be dragged independently. */
841 public void linkPatches() {
842 final String prop
= project
.getProperty(Project
.getName(this.getClass()).toLowerCase() + "_nolinks");
843 if (null != prop
&& prop
.equals("true")) return;
844 // find the patches that don't lay under other profiles of this profile's linking group, and make sure they are unlinked. This will unlink any Patch objects under this Profile:
845 unlinkAll(Patch
.class);
847 // scan the Display and link Patch objects that lay under this Profile's bounding box:
849 // catch all displayables of the current Layer
850 final ArrayList al
= layer
.getDisplayables(Patch
.class);
852 // this bounding box:
853 final Polygon perimeter
= getPerimeter(); //displaced by this object's position!
854 if (null == perimeter
) return; // happens when a profile with zero points is deleted
856 // for each Patch, check if it underlays this profile's bounding box
857 Rectangle box
= new Rectangle();
858 for (Iterator itd
= al
.iterator(); itd
.hasNext(); ) {
859 final Displayable displ
= (Displayable
)itd
.next();
860 // stupid java, Polygon cannot test for intersection with another Polygon !! //if (perimeter.intersects(displ.getPerimeter())) // TODO do it yourself: check if a Displayable intersects another Displayable
861 if (perimeter
.intersects(displ
.getBoundingBox(box
))) {
867 /** Unlink all Displayable objects of the given type linked by this. */
868 public void unlinkAll(final Class c
) {
869 if (!this.isLinked() || null == hs_linked
) {
872 // catch Displayables, or the iterators will go mad when deleting objects
873 final Displayable
[] displ
= new Displayable
[hs_linked
.size()];
874 hs_linked
.toArray(displ
);
875 for (int i
=0; i
<displ
.length
; i
++) {
876 if (displ
[i
].getClass() == c
) {
882 /** Check if this perimeter's intersects that of the given Displayable. */
883 public boolean intersects(final Displayable d
) {
884 return intersects(new Area(d
.getPerimeter()));
887 public boolean intersects(final Area area
) {
888 final Area a
= new Area(getPerimeter());
890 final Rectangle b
= a
.getBounds();
891 return 0 != b
.width
&& 0 != b
.height
;
894 /** Calls intersects(area) unless overriden -- intended for ZDisplayable objects to return whether they intersect the given area at the given layer. */
895 public boolean intersects(final Layer layer
, final Area area
) {
896 return intersects(area
);
899 public boolean intersects(final Layer layer
, final Rectangle r
) {
900 return getBoundingBox(null).intersects(r
);
903 /** Returns the intersection of this Displayable's area with the given one. */
904 public Area
getIntersection(final Displayable d
) {
905 final Area a
= new Area(this.getPerimeter());
906 a
.intersect(new Area(d
.getPerimeter()));
910 /** Returns the sum of bounding boxes of all linked Displayables. */
911 public Rectangle
getLinkedBox(final boolean same_layer
) {
912 if (null == hs_linked
|| hs_linked
.isEmpty()) return getBoundingBox();
913 final Rectangle box
= new Rectangle();
914 accumulateLinkedBox(same_layer
, new HashSet(), box
);
918 /** Accumulates in the box. */
919 private void accumulateLinkedBox(final boolean same_layer
, final HashSet hs_done
, final Rectangle box
) {
920 if (hs_done
.contains(this)) return;
922 box
.add(getBoundingBox(null));
923 for (final Displayable d
: hs_linked
) {
924 // add ZDisplayables regardless, for their 'layer' pointer is used to know which part of them must be painted.
925 if (same_layer
&& !(d
instanceof ZDisplayable
) && d
.layer
!= this.layer
) continue;
926 d
.accumulateLinkedBox(same_layer
, hs_done
, box
);
930 /** Minimal info that identifies this object as unique, for display on a JTree node.*/
931 public String
toString() {
932 return new StringBuffer(this.title
.length() + 20).append(this.title
).append(!(this instanceof ZDisplayable
) && null != layer ?
" z=" + layer
.getZ() : "").append(' ').append('#').append(this.id
).toString(); // the layer is null when recreating the object from the database and printing it for testing in the Loader
935 abstract public boolean isDeletable();
937 /** Subclasses can specify the behaviour, for the default is true.*/
938 public boolean canSendTo(Layer layer
) {
942 /** Does nothing unless overriden. */
943 public void exportSVG(StringBuffer data
, double z_scale
, String indent
) {}
945 /** Does nothing unless overriden. Used for profile, pipe and ball points when preventing dragging beyond the screen, to snap to cursor when this reenters. */
946 public void snapTo(int cx
, int cy
, int x_p
, int y_p
) {}
948 /** Shows a dialog to adjust properties of this object. */
949 public void adjustProperties() {
950 GenericDialog gd
= makeAdjustPropertiesDialog();
952 if (gd
.wasCanceled()) return;
953 processAdjustPropertiesDialog(gd
);
956 protected GenericDialog
makeAdjustPropertiesDialog() {
957 Rectangle box
= getBoundingBox(null);
958 GenericDialog gd
= new GD("Properties", this);
959 gd
.addStringField("title: ", title
);
960 gd
.addNumericField("x: ", box
.x
, 2);
961 gd
.addNumericField("y: ", box
.y
, 2);
962 gd
.addNumericField("scale_x: ", 1, 2);
963 gd
.addNumericField("scale_y: ", 1, 2);
964 gd
.addNumericField("rot (degrees): ", 0, 2);
965 gd
.addSlider("alpha: ", 0, 100, (int)(alpha
*100));
966 gd
.addCheckbox("visible", visible
);
967 gd
.addSlider("Red: ", 0, 255, color
.getRed());
968 gd
.addSlider("Green: ", 0, 255, color
.getGreen());
969 gd
.addSlider("Blue: ", 0, 255, color
.getBlue());
970 gd
.addCheckbox("locked", locked
);
971 // add slider listener
972 final Scrollbar alp
= (Scrollbar
)gd
.getSliders().get(0);
973 final Scrollbar red
= (Scrollbar
)gd
.getSliders().get(1);
974 final Scrollbar green
= (Scrollbar
)gd
.getSliders().get(2);
975 final Scrollbar blue
= (Scrollbar
)gd
.getSliders().get(3);
976 final TextField talp
= (TextField
)gd
.getNumericFields().get(5);
977 final TextField tred
= (TextField
)gd
.getNumericFields().get(6);
978 final TextField tgreen
= (TextField
)gd
.getNumericFields().get(7);
979 final TextField tblue
= (TextField
)gd
.getNumericFields().get(8);
980 SliderListener sla
= new SliderListener() {
981 public void update() {
982 setAlpha((float)alp
.getValue()/100);
985 SliderListener slc
= new SliderListener() {
986 public void update() {
987 setColor(new Color(red
.getValue(), green
.getValue(), blue
.getValue()));
990 alp
.addAdjustmentListener(sla
);
991 red
.addAdjustmentListener(slc
);
992 green
.addAdjustmentListener(slc
);
993 blue
.addAdjustmentListener(slc
);
994 talp
.addTextListener(sla
);
995 tred
.addTextListener(slc
);
996 tgreen
.addTextListener(slc
);
997 tblue
.addTextListener(slc
);
1001 private abstract class SliderListener
implements AdjustmentListener
, TextListener
{
1002 public void adjustmentValueChanged(AdjustmentEvent ae
) { update(); }
1003 public void textValueChanged(TextEvent te
) { update(); }
1004 abstract public void update();
1007 private class GD
extends GenericDialog
{
1011 GD(String title
, Displayable displ
) {
1014 this.dcolor
= new Color(displ
.color
.getRed(), displ
.color
.getGreen(), displ
.color
.getBlue()); // can't clone color?
1015 this.dalpha
= displ
.alpha
;
1017 /** Override to restore original color when canceled. */
1018 public void dispose() {
1019 if (wasCanceled()) {
1020 displ
.alpha
= dalpha
;
1021 displ
.setColor(dcolor
); // calls repaint
1027 protected DoEdit
processAdjustPropertiesDialog(final GenericDialog gd
) {
1028 // store old transforms for undo
1029 HashSet
<Displayable
> hs
= getLinkedGroup(new HashSet
<Displayable
>());
1030 layer
.getParent().addTransformStep(hs
);
1032 //Rectangle box = getLinkedBox(true);//getBoundingBox();
1034 final HashSet
<String
> fields
= new HashSet
<String
>();
1037 String title1
= gd
.getNextString();
1038 double x1
= gd
.getNextNumber();
1039 double y1
= gd
.getNextNumber();
1040 double sx
= gd
.getNextNumber();
1041 double sy
= gd
.getNextNumber();
1042 double rot1
= gd
.getNextNumber();
1043 float alpha1
= (float)gd
.getNextNumber() / 100;
1045 final DoEdit prev
= new DoEdit(this);
1047 if (Double
.isNaN(x1
) || Double
.isNaN(y1
) || Double
.isNaN(sx
) || Double
.isNaN(sy
) || Float
.isNaN(alpha1
)) {
1048 Utils
.showMessage("Invalid values!");
1052 Color co
= new Color((int)gd
.getNextNumber(), (int)gd
.getNextNumber(), (int)gd
.getNextNumber());
1053 if (!co
.equals(this.color
)) {
1054 prev
.add("color", color
);
1056 updateInDatabase("color");
1058 boolean visible1
= gd
.getNextBoolean();
1059 boolean locked1
= gd
.getNextBoolean();
1060 if (!title
.equals(title1
)) {
1061 prev
.add("title", title1
);
1062 setTitle(title1
); // will update the panel
1066 // Add the transforms, even if not modified (too much pain)
1067 if (null != hs
) prev
.add(new DoTransforms().addAll(hs
));
1068 else prev
.add("at", this.getAffineTransformCopy());
1070 final Rectangle b
= getBoundingBox(null); // previous
1071 if (x1
!= b
.x
|| y1
!= b
.y
) {
1073 // apply the scaling and displacement to all linked
1074 Rectangle box_old
= getBoundingBox();
1075 // fix FreeHandProfile past errors
1076 if (0 == box_old
.width
|| 0 == box_old
.height
) {
1077 if (this instanceof Profile
) {
1078 ((Profile
)this).calculateBoundingBox(true);
1079 box_old
= getBoundingBox();
1081 //avoid division by zero
1082 Utils
.showMessage("Some error ocurred: zero width or height ob the object to adjust.\nUnlink this object '" + this + "' and adjust carefully");
1086 this.setLocation(x1
, y1
);
1087 Rectangle b2
= getBoundingBox(null);
1088 int dx
= b2
.x
- b
.x
;
1089 int dy
= b2
.y
- b
.y
;
1090 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1091 Displayable d
= (Displayable
)it
.next();
1092 if (this.equals(d
)) continue;
1093 d
.translate(dx
, dy
, false);
1096 this.setLocation(x1
, y1
);
1099 if (1 != sx
|| 1 != sy
) {
1102 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1103 Displayable d
= (Displayable
)it
.next();
1104 d
.scale(sx
, sy
, b
.y
+b
.width
/2, b
.y
+b
.height
/2, false); // centered on this
1107 this.scale(sx
, sy
, b
.y
+b
.width
/2, b
.y
+b
.height
/2, false);
1111 // rotate relative to the center of he Displayable
1112 double rads
= Math
.toRadians(rot1
);
1114 //Utils.log2("delta_angle, rot1, rot: " + delta_angle + "," + rot1 + "," + rot);
1115 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1116 Displayable d
= (Displayable
)it
.next();
1117 d
.rotate(rads
, b
.x
+b
.width
/2, b
.y
+b
.height
/2, false);
1120 this.rotate(rads
, b
.x
+b
.width
/2, b
.y
+b
.height
/2, false);
1123 if (alpha1
!= alpha
) {
1124 prev
.add("alpha", alpha1
);
1125 setAlpha(alpha1
, true);
1127 if (visible1
!= visible
) {
1128 prev
.add("visible", visible1
);
1129 setVisible(visible1
);
1131 if (locked1
!= locked
) {
1132 prev
.add("locked", locked1
);
1136 getLayerSet().addEditStep(prev
); // contains the transformations of all others, if necessary.
1138 // it's lengthy to predict the precise box for each open Display, so just repaint all in all Displays.
1139 Display
.updateSelection();
1140 Display
.repaint(getLayer()); // not this.layer, so ZDisplayables are repainted properly
1145 static protected final String TAG_ATTR1
= "<!ATTLIST ";
1146 static protected final String TAG_ATTR2
= " NMTOKEN #REQUIRED>\n";
1148 /** Adds simply DTD !ATTRIBUTE tags. The ProjectThing that encapsulates this object will give the type. */
1149 static public void exportDTD(String type
, StringBuffer sb_header
, HashSet hs
, String indent
) {
1150 sb_header
.append(indent
).append(TAG_ATTR1
).append(type
).append(" oid").append(TAG_ATTR2
)
1151 .append(indent
).append(TAG_ATTR1
).append(type
).append(" layer_id").append(TAG_ATTR2
)
1153 .append(indent).append(TAG_ATTR1).append(type).append(" x").append(TAG_ATTR2)
1154 .append(indent).append(TAG_ATTR1).append(type).append(" y").append(TAG_ATTR2)
1155 .append(indent).append(TAG_ATTR1).append(type).append(" width").append(TAG_ATTR2)
1156 .append(indent).append(TAG_ATTR1).append(type).append(" height").append(TAG_ATTR2)
1158 .append(indent
).append(TAG_ATTR1
).append(type
).append(" transform").append(TAG_ATTR2
)
1159 .append(indent
).append(TAG_ATTR1
).append(type
).append(" style").append(TAG_ATTR2
)
1160 .append(indent
).append(TAG_ATTR1
).append(type
).append(" locked").append(TAG_ATTR2
)
1161 .append(indent
).append(TAG_ATTR1
).append(type
).append(" visible").append(TAG_ATTR2
)
1162 .append(indent
).append(TAG_ATTR1
).append(type
).append(" title").append(TAG_ATTR2
)
1163 .append(indent
).append(TAG_ATTR1
).append(type
).append(" links").append(TAG_ATTR2
)
1167 static public void exportDTD(StringBuffer sb_header
, HashSet hs
, String indent
) {
1168 if (!hs
.contains("t2_prop")) {
1169 sb_header
.append(indent
).append("<!ELEMENT t2_prop EMPTY>\n")
1170 .append(indent
).append(TAG_ATTR1
).append("t2_prop key").append(TAG_ATTR2
)
1171 .append(indent
).append(TAG_ATTR1
).append("t2_prop value").append(TAG_ATTR2
)
1174 if (!hs
.contains("t2_linked_prop")) {
1175 sb_header
.append(indent
).append("<!ELEMENT t2_linked_prop EMPTY>\n")
1176 .append(indent
).append(TAG_ATTR1
).append("t2_linked_prop target_id").append(TAG_ATTR2
)
1177 .append(indent
).append(TAG_ATTR1
).append("t2_linked_prop key").append(TAG_ATTR2
)
1178 .append(indent
).append(TAG_ATTR1
).append("t2_linked_prop value").append(TAG_ATTR2
)
1183 static protected String
commonDTDChildren() {
1184 return "t2_prop,t2_linked_prop"; // never commas at beginning or end, only in between
1185 // never returns empty
1188 /** The oid is this objects' id, whereas the 'id' tag will be the id of the wrapper Thing object. */ // width and height are used for the data itself, so that for example the image does not need to be loaded
1189 public void exportXML(final StringBuffer sb_body
, final String in
, final Object any
) {
1190 final double[] a
= new double[6];
1192 sb_body
.append(in
).append("oid=\"").append(id
).append("\"\n")
1193 .append(in
).append("width=\"").append(width
).append("\"\n")
1194 .append(in
).append("height=\"").append(height
).append("\"\n")
1195 .append(in
).append("transform=\"matrix(").append(a
[0]).append(',')
1196 .append(a
[1]).append(',')
1197 .append(a
[2]).append(',')
1198 .append(a
[3]).append(',')
1199 .append(a
[4]).append(',')
1200 .append(a
[5]).append(")\"\n")
1202 // the default is obvious, so just store the value if necessary
1203 if (locked
) sb_body
.append(in
).append("locked=\"true\"\n");
1204 if (!visible
) sb_body
.append(in
).append("visible=\"false\"\n");
1205 // 'style' is taken care in subclasses
1206 if (null != title
&& title
.length() > 0) {
1207 sb_body
.append(in
).append("title=\"").append(title
.replaceAll("\"", "^#^")).append("\"\n"); // fix possible harm by '"' characters (backslash should be taken care of as well TODO)
1209 sb_body
.append(in
).append("links=\"");
1210 if (null != hs_linked
&& 0 != hs_linked
.size()) {
1211 // Sort the ids: so resaving the file saves an identical file (otherwise, ids are in different order).
1212 final long[] ids
= new long[hs_linked
.size()];
1214 for (final Displayable d
: hs_linked
) ids
[ii
++] = d
.id
;
1216 for (int g
=0; g
<ids
.length
; g
++) sb_body
.append(ids
[g
]).append(',');
1217 sb_body
.setLength(sb_body
.length()-1); // remove last comma by shifting cursor backwards
1219 sb_body
.append("\"\n");
1222 /** Add properties, links, etc. Does NOT close the tag. */
1223 synchronized protected void restXML(final StringBuffer sb_body
, final String in
, final Object any
) {
1225 if (null != props
&& !props
.isEmpty()) {
1226 for (final Map
.Entry
<String
,String
> e
: props
.entrySet()) {
1227 final String value
= e
.getValue();
1228 if (null == value
) continue; // impossible, but with reflection one may set it so
1229 sb_body
.append(in
).append("<t2_prop key=\"").append(e
.getKey()).append("\" value=\"").append(cleanAttr(e
, value
)).append("\" />\n");
1232 if (null != linked_props
&& !linked_props
.isEmpty()) {
1233 for (final Map
.Entry
<Displayable
,Map
<String
,String
>> et
: linked_props
.entrySet()) {
1234 final Displayable target
= et
.getKey();
1235 for (final Map
.Entry
<String
,String
> e
: et
.getValue().entrySet()) {
1236 final String value
= e
.getValue();
1237 if (null == value
) continue; // impossible, but with reflection one may set it so
1238 sb_body
.append(in
).append("<t2_linked_prop target_id=\"").append(target
.id
).append("\" key=\"").append(e
.getKey()).append("\" value=\"").append(cleanAttr(e
, value
)).append("\" />\n");
1244 // Make sure the value is valid for an XML attribute content inside double quotes.
1245 final private String
cleanAttr(final Map
.Entry
<String
,String
> e
, String value
) {
1246 if (-1 != value
.indexOf('"')) {
1247 Utils
.log("Property " + e
.getKey() + " for ob id=#" + this.id
+ " contains a \" which is being replaced by '.");
1248 value
= value
.replace('"', '\'');
1250 if (-1 != value
.indexOf('\n')) {
1251 Utils
.log("Property " + e
.getKey() + " for ob id=#" + this.id
+ " contains a newline char which is being replaced by a space.");
1252 value
= value
.replace('\n', ' ');
1257 // I'm sure it could be made more efficient, but I'm too tired!
1258 public boolean hasLinkedGroupWithinLayer(Layer la
) {
1259 HashSet hs
= getLinkedGroup(new HashSet());
1260 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1261 Displayable d
= (Displayable
)it
.next();
1262 if (!d
.layer
.equals(la
)) return false;
1267 /** Rotate 2D points relative to the given pivot point by the given rot angle (in radians), in the 2D plane. */
1268 static public double[][] rotatePoints(double[][] p
, double rot
, double xo
, double yo
) {
1269 //Utils.log("calling rotatePoints for " + p + " with rot=" + Math.toDegrees(rot));
1271 Utils
.log2("WARNING: Displayable.rotatePoints received a null points array.");
1272 return new double[0][0];
1274 int length
= p
[0].length
;
1275 double[][] pr
= new double[p
.length
][length
];
1276 for (int i
=0; i
<length
; i
++) {
1277 // catch for readability
1280 // angle relative to the pivot point
1281 double b1
= M
.getAngle(x
- xo
, y
- yo
);
1282 // new angle relative to pivot point
1283 double b2
= b1
+ rot
;
1285 double hypot
= Math
.sqrt((x
- xo
)*(x
- xo
) + (y
- yo
)*(y
- yo
));
1286 pr
[0][i
] = xo
+ (Math
.cos(b2
) * hypot
); //x + (Math.cos(b2) - Math.cos(b1)) * hypot;
1287 pr
[1][i
] = yo
+ (Math
.sin(b2
) * hypot
); //y + (Math.sin(b2) - Math.sin(b1)) * hypot;
1292 /** Scale 2D points relative to the given pivot point xo,yo. */
1293 static public double[][] scalePoints(double[][] p
, double sx
, double sy
, double xo
, double yo
) {
1294 int length
= p
[0].length
;
1295 double[][] ps
= new double[p
.length
][length
];
1296 for (int i
=0; i
<length
; i
++) {
1297 ps
[0][i
] = xo
+ (p
[0][i
] - xo
) * sx
;
1298 ps
[1][i
] = yo
+ (p
[1][i
] - yo
) * sy
;
1303 static public double[][] displacePoints(double[][] p
, double dx
, double dy
) {
1304 int length
= p
[0].length
;
1305 double[][] pd
= new double[p
.length
][length
];
1306 for (int i
=0; i
<length
; i
++) {
1307 pd
[0][i
] = p
[0][i
] + dx
;
1308 pd
[1][i
] = p
[1][i
] + dy
;
1313 /** Transform in place only the 'i' point in the points array.*/
1314 static public void transformPoint(double[][] p
, int i
, double dx
, double dy
, double rot
, double xo
, double yo
) {
1318 double hypot
= Math
.sqrt(Math
.pow(p
[0][i
] - xo
, 2) + Math
.pow(p
[1][i
] - yo
, 2));
1319 double angle
= M
.getAngle(p
[0][i
] - xo
, p
[1][i
] - yo
);
1320 p
[0][i
] = xo
+ Math
.cos(angle
+ rot
) * hypot
;
1321 p
[1][i
] = yo
+ Math
.sin(angle
+ rot
) * hypot
;
1325 /** Fine nearest point in array a, from 0 up to n_points, to point x_p,y_p.
1326 * @return the index of such point. */
1327 static protected int findNearestPoint(final double[][] a
, final int n_points
, final double x_p
, final double y_p
) {
1328 if (0 == n_points
) return -1;
1329 double min_dist
= Double
.MAX_VALUE
;
1331 for (int i
=0; i
<n_points
; i
++) {
1332 double sq_dist
= Math
.pow(a
[0][i
] - x_p
, 2) + Math
.pow(a
[1][i
] - y_p
, 2);
1333 if (sq_dist
< min_dist
) {
1341 /** Performs a deep copy of this object. */
1342 public Displayable
clone() {
1343 return clone(this.project
);
1346 /** Performs a deep copy of this object, obtaining its unique id either from the given project or the exact same as this object's id. The visibility, though, is set to true at all times. */
1347 abstract public Displayable
clone(Project pr
, boolean copy_id
);
1349 /** Performs a deep copy of this object but assigning to it the given project. The visibility, though, is set to true at all times. */
1350 public Displayable
clone(Project pr
) {
1351 return clone(pr
, false);
1354 public LayerSet
getLayerSet() {
1355 if (null != layer
) return layer
.getParent();
1359 public boolean updateInDatabase(String key
) {
1360 // ???? TODO ???? cruft from the past? // project.getLoader().updateCache(this, key);
1361 //if (Utils.java3d) Display3D.update(this);
1362 return super.updateInDatabase(key
);
1365 static public Rectangle
getMinimalBoundingBox(Displayable
[] d
) {
1366 final Rectangle box
= d
[0].getBoundingBox();
1367 final Rectangle tmp
= new Rectangle();
1368 for (int i
=1; i
<d
.length
; i
++) {
1369 box
.add(d
[i
].getBoundingBox(tmp
));
1374 public AffineTransform
getAffineTransform() {
1378 public AffineTransform
getAffineTransformCopy() {
1379 return (AffineTransform
)at
.clone();
1382 /** Sets the matrix values of this Displayable's AffineTransform to those of the given AffineTransform. */
1383 public void setAffineTransform(AffineTransform at
) {
1384 this.at
.setTransform(at
);
1385 updateInDatabase("transform");
1389 /** Translate this Displayable and its linked ones if linked=true. */
1390 public void translate(double dx
, double dy
, boolean linked
) {
1391 if (Double
.isNaN(dx
) || Double
.isNaN(dy
)) return;
1392 final AffineTransform at2
= new AffineTransform();
1393 at2
.translate(dx
, dy
);
1394 preTransform(at2
, linked
);
1397 public void translate(double dx
, double dy
) {
1398 translate(dx
, dy
, true);
1401 /** Rotate relative to an anchor point. */
1402 public void rotate(double radians
, double xo
, double yo
) {
1403 rotate(radians
, xo
, yo
, true);
1406 /** Rotate relative to an anchor point. */
1407 public void rotate(double radians
, double xo
, double yo
, boolean linked
) {
1408 if (Double
.isNaN(radians
) || Double
.isNaN(xo
) || Double
.isNaN(yo
)) return;
1409 final AffineTransform at2
= new AffineTransform();
1410 at2
.rotate(radians
, xo
, yo
);
1411 preTransform(at2
, linked
);
1414 /** Commands the parent container (a Layer or a LayerSet) to update the bucket position of this Displayable. */
1415 public void updateBucket() {
1416 if (null != getBucketable()) getBucketable().updateBucket(this);
1419 /** Scale relative to an anchor point (will translate as necessary). */
1420 public void scale(double sx
, double sy
, double xo
, double yo
) {
1421 scale(sx
, sy
, xo
, yo
, true);
1424 /** Scale relative to an anchor point (will translate as necessary). */
1425 public void scale(double sx
, double sy
, double xo
, double yo
, boolean linked
) {
1426 if (Double
.isNaN(sx
) || Double
.isNaN(sy
) || Double
.isNaN(xo
) || Double
.isNaN(yo
)) return;
1427 final AffineTransform at2
= new AffineTransform();
1428 at2
.translate( xo
, yo
);
1429 at2
.scale( sx
, sy
);
1430 at2
.translate( -xo
, -yo
);
1431 preTransform(at2
, linked
);
1434 /** Sets the top left of the bounding box to x,y. Warning: does not check that the object will remain within layer bounds. Does NOT affect linked Displayables. */
1435 public void setLocation(double x
, double y
) {
1436 if (Double
.isNaN(x
) || Double
.isNaN(y
)) return;
1437 Rectangle b
= getBoundingBox(null);
1438 this.translate(x
- b
.x
, y
- b
.y
, false); // do not affect linked Displayables
1439 //Utils.log2("setting new loc, args are: " + x + ", "+ y);
1443 /** Apply this Displayable's AffineTransform to the given point. */
1444 public Point2D
.Double
transformPoint(final double px
, final double py
) {
1445 final Point2D
.Double pSrc
= new Point2D
.Double(px
, py
);
1446 if (this.at
.isIdentity()) return pSrc
;
1447 final Point2D
.Double pDst
= new Point2D
.Double();
1448 this.at
.transform(pSrc
, pDst
);
1452 public Point2D
.Double
inverseTransformPoint(final double px
, final double py
) {
1453 final Point2D
.Double pSrc
= new Point2D
.Double(px
, py
);
1454 if (this.at
.isIdentity()) return pSrc
;
1455 final Point2D
.Double pDst
= new Point2D
.Double();
1457 //this.at.createInverse().transform(pSrc, pDst);
1458 this.at
.inverseTransform(pSrc
, pDst
);
1459 } catch (NoninvertibleTransformException nite
) {
1460 IJError
.print(nite
);
1465 /** Returns a new Rectangle which encloses completly the given rectangle after transforming it with this Displayable's AffineTransform. The given rectangle's fields are untouched.*/
1466 final public Rectangle
transformRectangle(final Rectangle r
) {
1467 if (this.at
.isIdentity()) return (Rectangle
)r
.clone();
1468 return new Area(r
).createTransformedArea(this.at
).getBounds();
1471 /** Returns the argument if this Displayable's AffineTransform is the identity; otherwise returns a new double[][] with all points from @param p transformed according to the AffineTransform. The double[][] array provided as argument is expected to be of type [2][length], i.e. two arrays describing x and y, and it is left intact.
1473 public double[][] transformPoints(final double[][] p
) {
1474 return transformPoints(p
, p
[0].length
);
1477 /** Will crop second dimension of the given array at the given length. */
1478 protected double[][] transformPoints(final double[][] p
, final int length
) {
1479 if (this.at
.isIdentity()) return p
;
1480 //final int length = p[0].length;
1481 final double[] p2a
= new double[length
* 2];
1482 for (int i
=0, j
=0; i
<length
; i
++, j
+=2) {
1486 final double[] p2b
= new double[length
* 2];
1487 this.at
.transform(p2a
, 0, p2b
, 0, length
); // what a silly format: consecutive x,y numbers! Clear case of premature optimization.
1488 final double[][] p3
= new double[2][length
];
1489 for (int i
=0, j
=0; i
<length
; i
++, j
+=2) {
1491 p3
[1][i
] = p2b
[j
+1];
1496 /** Concatenate the given affine to this and all its linked objects. */
1497 public void transform(final AffineTransform at
) {
1498 for (Iterator it
= getLinkedGroup(new HashSet()).iterator(); it
.hasNext(); ) {
1499 Displayable d
= (Displayable
)it
.next();
1500 d
.at
.concatenate(at
);
1501 d
.updateInDatabase("transform");
1503 //Utils.log("applying transform to " + d);
1507 /** preConcatenate the given affine transform to this Displayable's affine. */
1508 public void preTransform(final AffineTransform affine
, final boolean linked
) {
1510 for (Iterator it
= getLinkedGroup(null).iterator(); it
.hasNext(); ) {
1511 final Displayable d
= (Displayable
)it
.next();
1512 d
.at
.preConcatenate(affine
);
1513 d
.updateInDatabase("transform");
1517 this.at
.preConcatenate(affine
);
1518 this.updateInDatabase("transform");
1519 this.updateBucket();
1523 public void paintAsBox(final Graphics2D g
) {
1524 final double[] c
= new double[]{0,0, width
,0, width
,height
, 0,height
};
1525 final double[] c2
= new double[8];
1526 this.at
.transform(c
, 0, c2
, 0, 4);
1528 g
.drawLine((int)c2
[0], (int)c2
[1], (int)c2
[2], (int)c2
[3]);
1529 g
.drawLine((int)c2
[2], (int)c2
[3], (int)c2
[4], (int)c2
[5]);
1530 g
.drawLine((int)c2
[4], (int)c2
[5], (int)c2
[6], (int)c2
[7]);
1531 g
.drawLine((int)c2
[6], (int)c2
[7], (int)c2
[0], (int)c2
[1]);
1534 public void paintSnapshot(final Graphics2D g
, final double mag
) {
1535 switch (layer
.getParent().getSnapshotsMode()) {
1537 paint(g
, mag
, false, 0xffffffff, layer
);
1542 default: return; // case 2: // disabled, no paint
1546 public DBObject
findById(final long id
) {
1547 if (this.id
== id
) return this;
1551 /** Does nothing unless overriden. */
1552 public ResultsTable
measure(ResultsTable rt
) {
1553 Utils
.showMessage("Not implemented yet for " + Project
.getName(getClass()) + " [class " + this.getClass().getName() + "]");
1557 public Bucketable
getBucketable() {
1561 /** If the title is purely numeric, returns it as a double; otherwise returns 0. */
1562 protected double getNameId() {
1564 if (null != this.title
) {
1566 nameid
= Double
.parseDouble(this.title
.trim());
1567 } catch (NumberFormatException nfe
) {}
1574 class DoEdits
implements DoStep
{
1575 final HashSet
<DoEdit
> edits
= new HashSet
<DoEdit
>();
1576 DoEdits(final Set
<Displayable
> col
) {
1577 for (final Displayable d
: col
) {
1578 edits
.add(new DoEdit(d
));
1581 public Displayable
getD() { return null; }
1582 public boolean isIdenticalTo(final Object ob
) {
1583 if (!(ob
instanceof DoEdits
)) return false;
1584 final DoEdits other
= (DoEdits
) ob
;
1585 if (this.edits
.size() != other
.edits
.size()) return false;
1586 final Iterator
<DoEdit
> it1
= this.edits
.iterator();
1587 final Iterator
<DoEdit
> it2
= other
.edits
.iterator();
1588 // Order matters: (but it shouldn't!) TODO
1589 for (; it1
.hasNext() && it2
.hasNext(); ) {
1590 if ( ! it1
.next().isIdenticalTo(it2
.next())) return false;
1594 public void init(final String
[] fields
) {
1595 for (final DoEdit edit
: edits
) {
1596 edit
.init(edit
.d
, fields
);
1599 public boolean isEmpty() { return edits
.isEmpty(); }
1600 public boolean apply(int action
) {
1601 boolean failed
= false;
1602 for (final DoEdit edit
: edits
) {
1603 if (!edit
.apply(action
)) {
1611 /** For any Displayable data, including: title, visible, locked, color, alpha,
1612 * and a 'data' type which includes the actual data (points, areas, etc.) and the links,width,height, and transformation (since all the latter are correlated).*/
1613 class DoEdit
implements DoStep
{
1614 private final HashMap
<String
,Object
> content
= new HashMap
<String
,Object
>();
1615 private ArrayList
<DoStep
> dependents
= null;
1616 private final Displayable d
;
1617 /** Returns self on success, otherwise null. */
1618 DoEdit(final Displayable d
) {
1621 public boolean containsKey(final String field
) {
1622 return content
.containsKey(field
);
1624 public boolean isIdenticalTo(final Object ob
) {
1625 if (!(ob
instanceof DoEdit
)) return false;
1626 final DoEdit other
= (DoEdit
) ob
;
1627 // same Displayable?
1628 if (this.d
!= other
.d
) return false;
1630 if (null != dependents
) {
1634 // same number of fields to edit?
1635 if (this.content
.size() != other
.content
.size()) return false;
1636 // any data? Currently comparisons of data are disabled
1637 if (null != this.content
.get("data") || null != other
.content
.get("data")) {
1640 // same content of fields?
1641 for (final Map
.Entry
<String
,Object
> e
: this.content
.entrySet()) {
1642 final Object val
= other
.content
.get(e
.getKey());
1644 Utils
.log2("WARNING: null val for " + e
.getKey());
1647 if (val
instanceof HashMap
) {
1648 if ( ! identical((HashMap
)val
, (HashMap
)e
.getValue())) {
1651 } else if ( ! val
.equals(e
.getValue())) return false;
1655 private boolean identical(final HashMap m1
, final HashMap m2
) {
1656 if (m1
.size() != m2
.size()) return false;
1657 for (final Object entry
: m1
.entrySet()) {
1658 final Map
.Entry e
= (Map
.Entry
) entry
;
1659 // TODO this could fail if value is null
1660 if ( ! e
.getValue().equals(m2
.get(e
.getKey()))) return false;
1664 synchronized public Displayable
getD() { return d
; }
1665 synchronized DoEdit
fullCopy() {
1666 return init(d
, new String
[]{"data", "width", "height", "locked", "title", "color", "alpha", "visible", "props", "linked_props"});
1668 /** With the same keys as 'de'. */
1669 synchronized DoEdit
init(final DoEdit de
) {
1670 return init(de
.d
, de
.content
.keySet().toArray(new String
[0]));
1672 synchronized public boolean add(final DoStep step
) {
1673 if (null == dependents
) dependents
= new ArrayList
<DoStep
>();
1674 if (dependents
.contains(step
)) return false;
1675 dependents
.add(step
);
1678 synchronized public boolean add(final String field
, final Object value
) {
1679 content
.put(field
, value
);
1682 synchronized DoEdit
init(final Displayable d
, final String
[] fields
) {
1683 final Class
[] c
= new Class
[]{Displayable
.class, d
.getClass(), ZDisplayable
.class};
1684 for (int k
=0; k
<fields
.length
; k
++) {
1685 if ("data".equals(fields
[k
])) {
1686 content
.put(fields
[k
], d
.getDataPackage());
1688 boolean got_it
= false;
1689 for (int i
=0; i
<c
.length
; i
++) {
1691 java
.lang
.reflect
.Field f
= c
[i
].getDeclaredField(fields
[k
]);
1692 if (null == f
) continue; // will throw a NoSuchFieldException, but just in case
1693 f
.setAccessible(true);
1694 Object ob
= f
.get(d
);
1695 content
.put(fields
[k
], null != ob ?
duplicate(ob
, fields
[k
]) : null);
1697 } catch (NoSuchFieldException nsfe
) {
1698 } catch (IllegalAccessException iae
) {}
1701 Utils
.log2("ERROR: could not get '" + fields
[k
] + "' field for " + d
);
1708 /** Java's clone() is useless. */ // I HATE this imperative, fragile, ridiculous language that forces me to go around in circles and O(n) approaches when all I need is a PersistentHashMap with structural sharing, a clone() that WORKS ALWAYS, and metaprogramming abilities aka macros @#$%!
1709 private final Object
duplicate(final Object ob
, final String field
) {
1710 if (ob
instanceof Color
) {
1711 final Color c
= (Color
)ob
;
1712 return new Color(c
.getRed(), c
.getGreen(), c
.getBlue());
1713 } else if (ob
instanceof HashMap
) {
1714 if (field
.equals("linked_props")) {
1715 final HashMap hm
= new HashMap();
1716 for (final Object e
: ((HashMap
)ob
).entrySet()) {
1717 final Map
.Entry me
= (Map
.Entry
)e
;
1718 hm
.put(me
.getKey(), ((HashMap
)me
.getValue()).clone());
1722 return new HashMap((HashMap
)ob
);
1724 // Number, Boolean, String are all final classes:
1727 /** Set the stored data to the stored Displayable. */
1728 public boolean apply(int action
) {
1729 final Class
[] c
= new Class
[]{Displayable
.class, d
.getClass(), ZDisplayable
.class};
1730 for (final Map
.Entry
<String
,Object
> e
: content
.entrySet()) {
1731 String field
= e
.getKey();
1732 if ("data".equals(field
)) {
1733 if (!d
.setDataPackage((DataPackage
)e
.getValue())) {
1738 for (int i
=0; i
<c
.length
; i
++) {
1739 java
.lang
.reflect
.Field f
= c
[i
].getDeclaredField(field
);
1740 f
.setAccessible(true);
1741 f
.set(d
, e
.getValue());
1743 } catch (NoSuchFieldException nsfe
) {
1744 } catch (IllegalAccessException iae
) {
1745 } catch (Exception ex
) {
1752 if (null != dependents
) {
1753 for (final DoStep step
: dependents
) {
1754 if (!step
.apply(action
)) ok
= false;
1757 Display
.update(d
.getLayerSet());
1760 public boolean isEmpty() {
1761 return null == d
|| (content
.isEmpty() && (null == dependents
|| dependents
.isEmpty()));
1765 protected class DoTransforms
implements DoStep
{
1766 final private HashMap
<Displayable
,AffineTransform
> ht
= new HashMap
<Displayable
,AffineTransform
>();
1767 final HashSet
<Layer
> layers
= new HashSet
<Layer
>();
1769 DoTransforms
addAll(final Collection
<Displayable
> col
) {
1770 for (final Displayable d
: col
) {
1771 ht
.put(d
, d
.getAffineTransformCopy());
1772 layers
.add(d
.getLayer());
1776 public boolean isEmpty() {
1777 return null == ht
|| ht
.isEmpty();
1779 public boolean apply(int action
) {
1780 if (isEmpty()) return false;
1781 for (final Map
.Entry
<Displayable
,AffineTransform
> e
: ht
.entrySet()) {
1782 e
.getKey().at
.setTransform(e
.getValue());
1784 for (final Layer layer
: layers
) {
1785 layer
.recreateBuckets();
1787 if (!layers
.isEmpty()) layers
.iterator().next().getParent().recreateBuckets(false);
1790 public Displayable
getD() { return null; }
1792 public boolean isIdenticalTo(final Object ob
) {
1793 if (ob
instanceof Collection
) {
1794 final Collection
<Displayable
> col
= (Collection
<Displayable
>) ob
;
1795 if (ht
.size() != col
.size()) return false;
1796 for (final Displayable d
: col
) {
1797 if (!d
.getAffineTransform().equals(ht
.get(d
))) {
1802 } else if (ob
instanceof DoTransforms
) {
1803 final DoTransforms dt
= (DoTransforms
) ob
;
1804 if (dt
.ht
.size() != this.ht
.size()) return false;
1805 for (final Map
.Entry
<Displayable
,AffineTransform
> e
: this.ht
.entrySet()) {
1806 if ( ! e
.getValue().equals(dt
.ht
.get(e
.getKey()))) {
1816 synchronized final boolean setDataPackage(final Displayable
.DataPackage pkg
) {
1817 if (pkg
.getClass() != getInternalDataPackageClass()) {
1818 Utils
.log2("ERROR: cannot set " + pkg
.getClass() + " to " + this.getClass());
1822 return pkg
.to2(this);
1824 } catch (Exception e
) {
1830 // Must be overriden by subclasses
1831 Object
getDataPackage() {
1832 Utils
.log2("Displayable.getDataPackage not implemented yet for " + getClass());
1835 // Must be overriden by subclasses
1836 Class
getInternalDataPackageClass() {
1837 return DataPackage
.class;
1840 static abstract protected class DataPackage
{
1841 protected final double width
, height
;
1842 protected final AffineTransform at
;
1843 protected HashMap
<Displayable
,HashSet
<Displayable
>> links
= null;
1845 DataPackage(final Displayable d
) {
1846 this.width
= d
.width
;
1847 this.height
= d
.height
;
1848 this.at
= new AffineTransform(d
.at
);
1849 if (null != d
.hs_linked
) {
1850 this.links
= new HashMap
<Displayable
,HashSet
<Displayable
>>();
1851 for (final Displayable ln
: d
.hs_linked
) {
1852 this.links
.put(ln
, new HashSet
<Displayable
>(ln
.hs_linked
));
1855 this.links
.put(d
, new HashSet
<Displayable
>(d
.hs_linked
));
1859 /** Set the Displayable's fields. */
1860 final boolean to1(final Displayable d
) {
1861 Utils
.log2("## to1");
1864 d
.setAffineTransform(at
); // updates bucket
1865 if (null != links
) {
1866 for (final Map
.Entry
<Displayable
,HashSet
<Displayable
>> e
: links
.entrySet()) {
1867 e
.getKey().hs_linked
= new HashSet
<Displayable
>(e
.getValue());
1868 Utils
.log2("setting links to " + d
);
1873 // Could simply use a single 'to' method (it works, tested),
1874 // but if I ever was to cast inadvertendly to Displayable, then
1875 // only the superclass' 'to' method would be invoked, not the
1876 // subclass' one! I call it "defensive programming"
1877 /** Set the subclass specific data fields. */
1878 abstract boolean to2(final Displayable d
);
1881 /** Returns true if any Displayable objects of different layers in sublist are linked to each other.
1882 * If ignore_stacks is true, then image links across layers are ignored. */
1883 static public final boolean areThereLayerCrossLinks(final Set
<Layer
> sublist
, final boolean ignore_stacks
) {
1884 if (null == sublist
|| 0 == sublist
.size()) return false;
1885 for (final Layer l
: sublist
) {
1886 for (final Displayable d
: l
.getDisplayables(Patch
.class)) {
1888 for (final Displayable other
: d
.getLinked()) {
1889 final Class c
= other
.getClass();
1890 if ( (!ignore_stacks
&& Patch
.class == c
&& other
.layer
!= d
.layer
)
1891 || (Profile
.class == c
&& other
.getLinked(Profile
.class).size() > 0)
1892 || ZDisplayable
.class.isAssignableFrom(c
)) {