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
.Search
;
39 import ini
.trakem2
.vector
.Compare
;
41 /** The class that any element to be drawn on a Display must extend. */
42 public abstract class Displayable
extends DBObject
{
44 final protected AffineTransform at
= new AffineTransform();
46 /** Width and height of the data, not the bounding box. If the AffineTransform is different than identity, then the bounding box will be different. */
47 protected double width
= 0,
50 protected boolean locked
= false;
51 protected String title
;
52 protected Color color
= Color
.yellow
;
53 protected float alpha
= 1.0f
; // from 0 to 1 (0 is full transparency)
54 protected boolean visible
= true;
55 protected Layer layer
;
56 /** The Displayable objects this one is linked to. Can be null. */
57 protected HashSet
<Displayable
> hs_linked
= null;
59 /** The table of key/value property pairs of this Displayable. */
60 protected Map
<String
,String
> props
= null;
62 /** 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. */
63 protected Map
<Displayable
,Map
<String
,String
>> linked_props
= null;
65 /** The call back hooks to remove any linked properties in other Displayable instances when this Displayable is removed. */
66 protected Set
<Displayable
> linked_props_origins
= null;
68 /** Set a key/valye property pair; to remove a property, set the value to null. */
69 synchronized public boolean setProperty(final String key
, final String value
) {
70 if (null == key
) return false;
71 if (null != props
&& null == value
) {
73 if (props
.isEmpty()) props
= null;
75 if (null == props
) props
= new HashMap
<String
,String
>();
76 props
.put(key
, value
);
81 /** If key is null or not found, returns null; otherwise returns the stored value for key. */
82 public String
getProperty(final String key
) {
83 return getProperty(key
, null);
86 /** If key is null or not found, returns default_value; otherwise returns the stored value for key. */
87 synchronized public String
getProperty(final String key
, final String default_value
) {
88 if (null == key
|| null == props
) return default_value
;
89 final String val
= props
.get(key
);
90 if (null == val
) return default_value
;
94 /** Returns a copy of this object's properties, or null if none. */
95 synchronized public Map
getProperties() {
96 if (null == props
) return null;
97 return new HashMap(props
);
100 /** 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. */
101 synchronized public boolean setLinkedProperty(final Displayable target
, final String key
, final String value
) {
102 if (null == target
|| null == key
) return false;
103 if (target
.project
!= this.project
) {
104 Utils
.log("Cannot link to a Displayable from another project!");
107 if (null != linked_props
&& null == value
) {
108 // Remove the key and cleanup if necessary
109 Map
<String
,String
> p
= linked_props
.get(target
);
113 linked_props
.remove(target
);
114 if (linked_props
.isEmpty()) {
120 linkedProps(target
).put(key
, value
);
125 /** 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. */
126 private final Map
<String
,String
> linkedProps(final Displayable target
) {
127 if (null == linked_props
) linked_props
= new HashMap
<Displayable
,Map
<String
,String
>>();
128 Map
<String
,String
> p
= linked_props
.get(target
);
130 p
= new HashMap
<String
,String
>();
131 linked_props
.put(target
, p
);
133 // Add hook on the target side: (so when the target is deleted, it will call removeLinkedProperties here)
134 if (null == target
.linked_props_origins
) {
135 target
.linked_props_origins
= new HashSet
<Displayable
>();
137 target
.linked_props_origins
.add(this);
142 /** 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. */
143 synchronized public void setLinkedProperties(final Displayable target
, final Map
<String
,String
> p
) {
144 if (null == target
|| null == p
) return;
145 if (target
.project
!= this.project
) {
146 Utils
.log("Cannot link to a Displayable from another project!");
149 final Map
<String
,String
> lp
= linkedProps(target
);
150 for (final Map
.Entry
<String
,String
> e
: p
.entrySet()) {
151 final String value
= e
.getValue();
153 // Remove key from table
154 lp
.remove(e
.getKey());
156 lp
.put(e
.getKey(), value
);
161 linked_props
.remove(target
);
165 /** Removes and returns the table of properties lined to the target Displayable. May be empty. */
166 synchronized public Map
<String
,String
> removeLinkedProperties(final Displayable target
) {
167 if (null == target
|| null == linked_props
) return new HashMap
<String
,String
>();
168 final Map
<String
,String
> p
= linked_props
.remove(target
);
169 if (linked_props
.isEmpty()) {
172 if (null == p
) return new HashMap
<String
,String
>();
176 /** Tell any other Displayable that has a linked property with this Displayable to remove it. */
177 protected void removeLinkedPropertiesFromOrigins() {
178 if (null != this.linked_props_origins
) {
179 for (final Displayable origin
: linked_props_origins
) {
180 origin
.removeLinkedProperties(this);
185 /** 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. */
186 public String
getLinkedProperty(final Displayable target
, final String key
) {
187 return getLinkedProperty(target
, key
, null);
190 /** 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. */
191 synchronized public String
getLinkedProperty(final Displayable target
, final String key
, final String default_value
) {
192 if (null == target
|| null == key
) return default_value
;
193 if (target
.project
!= this.project
) {
194 Utils
.log("You attempted to get a property for a Displayable of another project, which is impossible.");
195 return default_value
;
197 if (null == linked_props
) return default_value
;
198 final Map
<String
,String
> p
= linked_props
.get(target
);
199 if (null == p
) return default_value
;
200 final String value
= p
.get(key
);
201 if (null == value
) return default_value
;
205 /** Returns a copy of this object's linked properties table for the given Displayable, which may be empty. */
206 synchronized public Map
<String
,String
> getLinkedProperties(final Displayable target
) {
207 if (null == target
|| null == linked_props
) return null;
208 final Map
<String
,String
> m
= linked_props
.get(target
);
209 if (null == m
) return new HashMap
<String
,String
>();
210 return new HashMap
<String
,String
>(m
);
213 /** Returns a copy of this object's linked properties, which may be empty. */
214 synchronized public Map
<Displayable
,Map
<String
,String
>> getLinkedProperties() {
215 final Map
<Displayable
,Map
<String
,String
>> lp
= new HashMap
<Displayable
,Map
<String
,String
>>();
216 if (null == linked_props
) return lp
;
217 for (final Map
.Entry
<Displayable
,Map
<String
,String
>> e
: linked_props
.entrySet()) {
218 lp
.put(e
.getKey(), new HashMap
<String
,String
>(e
.getValue()));
223 ////////////////////////////////////////////////////
224 public void setLocked(boolean lock
) {
225 if (lock
) this.locked
= lock
;
227 // to unlock, unlock those in the linked group that are locked
229 unlockAllLinked(new HashSet());
231 updateInDatabase("locked");
233 private void unlockAllLinked(HashSet hs
) {
234 if (hs
.contains(this)) return;
236 if (null == hs_linked
) return;
237 for (final Displayable d
: hs_linked
) {
238 if (d
.locked
) d
.locked
= false;
239 d
.unlockAllLinked(hs
);
242 /** Return the value of the field 'locked'. */
243 public boolean isLocked2() {
246 /** Check if this or any of the Displayables in the linked group is locked. */
247 public boolean isLocked() {
248 if (locked
) return true;
249 return isLocked(new HashSet());
251 private boolean isLocked(HashSet hs
) {
252 if (locked
) return true;
253 else if (hs
.contains(this)) return false;
255 if (null != hs_linked
&& hs_linked
.size() > 0) {
256 for (final Displayable d
: hs_linked
) {
257 if (d
.isLocked(hs
)) return true;
262 ////////////////////////////////////////////////////
263 /** The minimal public Displayable constructor. */
264 public Displayable(Project project
, String title
, double x
, double y
) {
267 this.at
.translate(x
, y
);
270 /** Reconstruct a Displayable from the database. */
271 public Displayable(Project project
, long id
, String title
, boolean locked
, AffineTransform at
, double width
, double height
) {
274 this.locked
= locked
;
275 if (null != at
) this.at
.setTransform(at
);
277 this.height
= height
;
280 /** Reconstruct a Displayable from an XML entry. Used entries get removed from the HashMap. */
281 public Displayable(Project project
, long id
, HashMap ht
, HashMap ht_links
) {
283 double x
=0, y
=0, rot
=0; // for backward compatibility
284 this.layer
= null; // will be set later
285 // parse data // TODO this is weird, why not just call them, since no default values are set anyway
286 final ArrayList al_used_keys
= new ArrayList();
287 for (Iterator it
= ht
.entrySet().iterator(); it
.hasNext(); ) {
288 Map
.Entry entry
= (Map
.Entry
)it
.next();
289 String key
= (String
)entry
.getKey();
290 String data
= (String
)entry
.getValue();
292 if (key
.equals("width")) width
= Double
.parseDouble(data
);
293 else if (key
.equals("height")) height
= Double
.parseDouble(data
);
294 else if (key
.equals("transform")) {
295 final String
[] nums
= data
.substring(data
.indexOf('(')+1, data
.lastIndexOf(')')).split(",");
296 this.at
.setTransform(Double
.parseDouble(nums
[0]), Double
.parseDouble(nums
[1]),
297 Double
.parseDouble(nums
[2]), Double
.parseDouble(nums
[3]),
298 Double
.parseDouble(nums
[4]), Double
.parseDouble(nums
[5]));
299 //Utils.log2("at: " + this.at);
301 else if (key
.equals("locked")) locked
= data
.trim().toLowerCase().equals("true");
302 else if (key
.equals("visible")) visible
= data
.trim().toLowerCase().equals("true");
303 else if (key
.equals("style")) {
306 int i_start
= data
.indexOf("stroke-opacity:");
309 i_start
= data
.indexOf("fill-opacity:");
314 this.alpha
= Float
.parseFloat(data
.substring(i_start
+ 13, data
.indexOf(';', i_start
+ 13)).trim());
317 this.alpha
= Float
.parseFloat(data
.substring(i_start
+ 15, data
.indexOf(';', i_start
+ 15)).trim());
320 i_start
= data
.indexOf("stroke:");
324 i_start
= data
.indexOf("fill:");
327 i_start
= data
.indexOf('#', data
.indexOf(':', i_start
+4)); // at least 4 (for 'fill:'), max 7 (for 'stroke:')
328 i_end
= data
.indexOf(';', i_start
+1);
329 this.color
= Utils
.getRGBColorFromHex(data
.substring(i_start
+1, i_end
));
331 Utils
.log2("Can't parse color for id=" + id
);
332 this.color
= Color
.yellow
;
334 // avoid recording this key for deletion, for the DLabel to read it
336 } catch (Exception es
) {
337 if (null == this.color
) this.color
= Color
.yellow
;
338 Utils
.log("ERROR at reading style: " + es
);
340 } else if (key
.equals("links")) {
341 // This is hard one, must be stored until all objects exist and then processed
342 if (null != data
&& data
.length() > 0) ht_links
.put(this, data
);
343 } else if (key
.equals("title")) {
344 if (null != data
&& !data
.toLowerCase().equals("null")) {
345 this.title
= data
.replaceAll("^#^", "\""); // fix " and backslash characters
346 } else this.title
= null;
347 } else if (key
.equals("x")) {
348 x
= Double
.parseDouble(data
); // this could be done with reflection, but not all, hence this dullness
349 } else if (key
.equals("y")) {
350 y
= Double
.parseDouble(data
);
351 } else if (key
.equals("rot")) {
352 rot
= Double
.parseDouble(data
);
354 al_used_keys
.add(key
);
355 } catch (Exception ea
) {
356 Utils
.log(this + " : failed to read data for key '" + key
+ "':\n" + ea
);
359 for (Iterator it
= al_used_keys
.iterator(); it
.hasNext(); ) {
360 ht
.remove(it
.next());
363 // support old versions:
364 if (this.at
.isIdentity() && (0 != x
|| 0 != y
|| 0 != rot
)) {
365 this.at
.translate(x
, y
);
367 AffineTransform at2
= new AffineTransform();
368 at2
.rotate(Math
.toRadians(rot
), x
+ width
/2, y
+ height
/2);
369 this.at
.preConcatenate(at2
);
372 // scaling in old versions will be lost
375 public void paint(Graphics2D g
, double magnification
, boolean active
, int channels
, Layer active_layer
) {
376 Utils
.log2("paint g, magnification, active, channels, active_layer: not implemented yet for " + this.getClass());
379 /** 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. */
380 public void prePaint(Graphics2D g
, double magnification
, boolean active
, int channels
, Layer active_layer
) {
381 paint(g
, magnification
, active
, channels
, active_layer
);
384 /** Not accepted if zero or negative. Remakes the snapshot, updates the snapshot panel and the Display. */
385 public void setDimensions(double width
, double height
) {
386 setDimensions(width
, height
, true);
389 /** 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. */
390 public void setDimensions(double width
, double height
, boolean repaint
) {
391 if (width
<= 0 || height
<= 0) return;
392 Rectangle b
= getBoundingBox(null);
393 if (b
.width
== width
&& b
.height
== height
) return;
394 double sx
= width
/ (double)b
.width
;
395 double sy
= height
/ (double)b
.height
;
396 this.scale(sx
, sy
, b
.x
, b
.y
); // relative to top left corner
398 Display
.repaint(layer
, this, 5);
399 //done with above//Display.updatePanel(layer, this);
403 public void setLayer(Layer layer
, boolean update_db
) {
404 if (null == layer
|| this.layer
== layer
) return;
406 if (update_db
) updateInDatabase("layer_id");
409 public void setLayer(Layer layer
) {
410 setLayer(layer
, true);
413 public Layer
getLayer() {
417 /** Does not accept null or zero-length titles. */
418 public void setTitle(String title
) {
419 if (null == title
|| 0 == title
.length()) return;
421 Display
.updateTitle(layer
, this); // update the DisplayablePanel(s) that show this Patch
422 updateInDatabase("title");
425 public String
getTitle() {
429 public String
getShortTitle() {
430 Rectangle b
= getBoundingBox(null);
431 return "x=" + Utils
.cutNumber(b
.x
, 2) + " y=" + Utils
.cutNumber(b
.y
, 2) + (null != layer ?
" z=" + Utils
.cutNumber(layer
.getZ(), 2) : "");
434 /** Returns the x of the bounding box. */
435 public double getX() {
436 return getBoundingBox(null).x
;
439 /** Returns the y of the bounding box. */
440 public double getY() {
441 return getBoundingBox(null).y
;
444 /** Returns the width of the data. */
445 public double getWidth() {
449 /** Returns the height of the data. */
450 public double getHeight() {
454 /** Bounding box of the transformed data. */
455 public Rectangle
getBoundingBox() {
456 return getBoundingBox(null);
459 /** 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. */
460 public Rectangle
getBounds(final Rectangle r
, final Layer layer
) {
461 return getBoundingBox(r
);
464 /** Bounding box of the transformed data. Saves one Rectangle allocation, returns the same Rectangle, modified (or a new one if null). */
465 public Rectangle
getBoundingBox(final Rectangle r
) {
466 return getBounds(null != r ? r
: new Rectangle());
469 /** Bounding box of the transformed data. Saves one allocation, returns the same Rectangle, modified (or a new one if null). */
470 private final Rectangle
getBounds(final Rectangle r
) {
473 r
.width
= (int)this.width
;
474 r
.height
= (int)this.height
;
475 if (this.at
.getType() == AffineTransform
.TYPE_TRANSLATION
) {
476 r
.x
+= (int)this.at
.getTranslateX();
477 r
.y
+= (int)this.at
.getTranslateY();
479 //r = transformRectangle(r);
481 final double[] d1
= new double[]{0, 0, width
, 0, width
, height
, 0, height
};
482 final double[] d2
= new double[8];
483 this.at
.transform(d1
, 0, d2
, 0, 4);
485 double min_x
=Double
.MAX_VALUE
, min_y
=Double
.MAX_VALUE
, max_x
=-min_x
, max_y
=-min_y
;
486 for (int i
=0; i
<d2
.length
; i
+=2) {
487 if (d2
[i
] < min_x
) min_x
= d2
[i
];
488 if (d2
[i
] > max_x
) max_x
= d2
[i
];
489 if (d2
[i
+1] < min_y
) min_y
= d2
[i
+1];
490 if (d2
[i
+1] > max_y
) max_y
= d2
[i
+1];
494 r
.width
= (int)(max_x
- min_x
);
495 r
.height
= (int)(max_y
- min_y
);
500 /** Subclasses can override this method to provide the exact contour, otherwise it returns the transformed bounding box of the data. */
501 public Polygon
getPerimeter() {
502 if (this.at
.isIdentity() || this.at
.getType() == AffineTransform
.TYPE_TRANSLATION
) {
503 // return the bounding box as a polygon:
504 final Rectangle r
= getBoundingBox();
505 return new Polygon(new int[]{r
.x
, r
.x
+r
.width
, r
.x
+r
.width
, r
.x
},
506 new int[]{r
.y
, r
.y
, r
.y
+r
.height
, r
.y
+r
.height
},
509 // else, the rotated/sheared/scaled and translated bounding box:
510 final double[] po1
= new double[]{0,0, width
,0, width
,height
, 0,height
};
511 final double[] po2
= new double[8];
512 this.at
.transform(po1
, 0, po2
, 0, 4);
513 return new Polygon(new int[]{(int)po2
[0], (int)po2
[2], (int)po2
[4], (int)po2
[6]},
514 new int[]{(int)po2
[1], (int)po2
[3], (int)po2
[5], (int)po2
[7]},
518 /** Returns the perimeter enlarged in all West, North, East and South directions, in pixels.*/
519 public Polygon
getPerimeter(final int w
, final int n
, final int e
, final int s
) {
520 if (this.at
.isIdentity() || this.at
.getType() == AffineTransform
.TYPE_TRANSLATION
) {
521 // return the bounding box as a polygon:
522 final Rectangle r
= getBoundingBox();
523 return new Polygon(new int[]{r
.x
-w
, r
.x
+r
.width
+w
+e
, r
.x
+r
.width
+w
+e
, r
.x
-w
},
524 new int[]{r
.y
-n
, r
.y
-n
, r
.y
+r
.height
+n
+s
, r
.y
+r
.height
+n
+s
},
527 // else, the rotated/sheared/scaled and translated bounding box:
528 final double[] po1
= new double[]{-w
,-n
, width
+w
+e
,-n
, width
+w
+e
,height
+n
+s
, -w
,height
+n
+s
};
529 final double[] po2
= new double[8];
530 this.at
.transform(po1
, 0, po2
, 0, 4);
531 return new Polygon(new int[]{(int)po2
[0], (int)po2
[2], (int)po2
[4], (int)po2
[6]},
532 new int[]{(int)po2
[1], (int)po2
[3], (int)po2
[5], (int)po2
[7]},
536 /** Test whether the given point falls within the perimeter of this Displayable, considering the position x,y. Used by the DisplayCanvas mouse events. */
537 public boolean contains(final int x_p
, final int y_p
) {
538 return getPerimeter().contains(x_p
, y_p
);
541 /** 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. */
542 public boolean contains(final Layer layer
, final int x_p
, final int y_p
) {
543 return contains(x_p
, y_p
);
546 public void setAlpha(float alpha
) {
547 setAlpha(alpha
, true);
550 protected void setAlpha(float alpha
, boolean update
) {
551 if (alpha
!= this.alpha
&& alpha
>= 0.0f
&& alpha
<= 1.0f
) {
554 Display
.repaint(layer
, this, 5);
555 updateInDatabase("alpha");
556 Display3D
.setTransparency(this, alpha
);
561 public float getAlpha() { return alpha
; }
563 public Color
getColor() { return color
; }
565 /** Return the HashSet of directly linked Displayable objects. */
566 public HashSet
<Displayable
> getLinked() { return hs_linked
; }
568 /** Return those of Class c from among the directly linked. */
569 public HashSet
<Displayable
> getLinked(final Class c
) {
570 if (null == hs_linked
) return null;
571 final HashSet
<Displayable
> hs
= new HashSet
<Displayable
>();
572 for (final Displayable d
: hs_linked
) {
573 if (d
.getClass() == c
) hs
.add(d
);
578 /** Return the HashSet of all directly and indirectly linked objects. */
579 public HashSet
<Displayable
> getLinkedGroup(HashSet
<Displayable
> hs
) {
580 if (null == hs
) hs
= new HashSet
<Displayable
>();
581 else if (hs
.contains(this)) return hs
;
583 if (null == hs_linked
) return hs
;
584 for (final Displayable d
: hs_linked
) {
585 d
.getLinkedGroup(hs
);
590 public void mousePressed(MouseEvent me
, int x_p
, int y_p
, double mag
) {
591 Utils
.log2("mousePressed not implemented yet for " + this.getClass().getName());
593 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
) {
594 Utils
.log2("mouseDragged not implemented yet for " + this.getClass().getName());
596 public void mouseReleased(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_r
, int y_r
) {
597 Utils
.log2("mouseReleased not implemented yet for " + this.getClass().getName());
600 public void keyPressed(KeyEvent ke
) {
602 int key_code
= ke
.getKeyCode();
606 case KeyEvent
.VK_ENTER
: //End transform or bring ImageJ to front
609 case KeyEvent
.VK_UP
: //move one pixel up
610 translate(0, -1, true);
613 case KeyEvent
.VK_DOWN
: //move one pixel down
614 translate(0, 1, true);
617 case KeyEvent
.VK_LEFT
: //move one pixel left
618 translate(-1, 0, true);
621 case KeyEvent
.VK_RIGHT
: //move one pixel right
622 translate(1, 0, true);
625 case KeyEvent
.VK_ESCAPE
:
626 Display
.setActive(ke
, null);
631 Rectangle box
= getLinkedBox(true);
633 if (ke
.isConsumed() && KeyEvent
.VK_ESCAPE
!= key_code
) {
634 // no need to repaint the previous box, falls within 1 pixel (and here I add 5 on the margins)
635 Display
.repaint(this.layer
, box
, 5);
636 } //not repainting for ESC because it has been done already
639 public boolean isVisible() { return this.visible
; }
641 public final void setVisible(boolean visible
) {
642 setVisible(visible
, true);
645 public void setVisible(final boolean visible
, final boolean repaint
) {
646 if (visible
== this.visible
) {
647 // patching synch error
648 Display
.updateVisibilityCheckbox(layer
, this, null);
651 this.visible
= visible
;
653 //Display.setUpdateGraphics(layer, this);
654 Display
.repaint(layer
, this, 5);
656 updateInDatabase("visible");
657 Display
.updateVisibilityCheckbox(layer
, this, null);
660 /** Repaint this Displayable in all Display instances that are showing it. */
661 public void repaint() {
662 Display
.repaint(layer
, this, 5);
665 public void setColor(Color color
) {
666 if (null == color
|| color
.equals(this.color
)) return;
668 updateInDatabase("color");
669 Display
.repaint(layer
, this, 5);
670 Display3D
.setColor(this, color
);
673 /** Release resources. */
674 public void destroy() {
678 public boolean isOutOfRepaintingClip(final double magnification
, final Rectangle srcRect
, final Rectangle clipRect
) {
679 // 1 - check visibility
681 //if not visible, it's out, so return true:
684 final Rectangle box
= getBoundingBox(null); // includes rotation
685 // 2 - check if out of clipRect (clipRect is in screen coords, whereas srcRect is in offscreen coords)
686 if (null != clipRect
&& null != srcRect
) {
687 final int screen_x
= (int)((box
.x
-srcRect
.x
) * magnification
);
688 final int screen_y
= (int)((box
.y
-srcRect
.y
) * magnification
);
689 final int screen_width
= (int)(box
.width
* magnification
);
690 final int screen_height
= (int)(box
.height
* magnification
);
691 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
)) {
696 // 3 - check if out of srcRect
697 // This is included above anyway, but just in case there is no clipRect !
698 if (null != srcRect
) {
699 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
) {
704 // else: it's in, so paint!
708 /** For the DisplayNavigator. No srcRect or magnification considered. */
709 public boolean isOutOfRepaintingClip(Rectangle clipRect
, double scale
) {
710 // 1 - check visibility
712 //if not visible, it's out, so return true:
715 Rectangle box
= getBoundingBox(); // includes rotation
716 // 2 - check if out of clipRect
717 if (null != clipRect
) {
718 int screen_x
= (int)(box
.x
* scale
);
719 int screen_y
= (int)(box
.y
* scale
);
720 int screen_width
= (int)(box
.width
* scale
);
721 int screen_height
= (int)(box
.height
* scale
);
722 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
)) {
727 // else: it's in, so paint!
731 /** Remove also from the trees if present; does nothing more than remove(boolean) unless overriden. */
732 protected boolean remove2(final boolean check
) {
733 return remove(check
);
736 /** Remove from both the database and any Display that shows the Layer in which this Displayable is shown. */
737 public boolean remove(final boolean check
) {
738 if (super.remove(check
) && layer
.remove(this)) { // TODO there should be Displayable2D and Displayable3D, and each extend Displayable
740 removeLinkedPropertiesFromOrigins();
742 Compare
.remove(this);
746 Utils
.log("Failed to remove " + this.getClass().getName() + " " + this);
748 // WARNING -- the ZDisplayable.remove should take ALSO any changes I add here later
751 /** 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. */
752 public void link(final Displayable d
) {
756 /** 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.*/
757 public void link(final Displayable d
, final boolean update_database
) { // the boolean is used by the loader when reconstructing links.
758 if (this == d
) return;
759 if (null == this.hs_linked
) this.hs_linked
= new HashSet
<Displayable
>();
760 // link the other to this
761 this.hs_linked
.add(d
);
762 // link this to the other
763 if (null == d
.hs_linked
) d
.hs_linked
= new HashSet();
764 d
.hs_linked
.add(this);
765 // update the database
766 if (update_database
) project
.getLoader().addCrossLink(project
.getId(), this.id
, d
.id
);
769 /** Remove all links held by this Displayable.*/
770 public void unlink() {
771 if (null == this.hs_linked
) return;
772 final Displayable
[] displ
= new Displayable
[hs_linked
.size()];
773 hs_linked
.toArray(displ
);
775 // all these redundancy because of the [typical] 'concurrent modification exception'
776 for (int i
=0; i
<displ
.length
; i
++) {
779 this.hs_linked
= null;
782 /** Remove the link with the given Displayable, and tell the given Displayable to remove the link with this. */
783 public void unlink(final Displayable d
) {
784 //Utils.log("Displayable.unlink(Displayable)");
786 return; // should not happen
788 if (null == this.hs_linked
) return; // should not happen
789 // unlink the other from this, and this from the other
790 if (!( hs_linked
.remove(d
) && d
.hs_linked
.remove(this))) {
791 // signal database inconsistency (should not happen)
792 Utils
.log("Database inconsistency: two displayables had a non-reciprocal link. BEWARE of other errors.");
794 // update the database in any case
795 project
.getLoader().removeCrossLink(this.id
, d
.id
);
798 /** Check if this object is directly linked to any other Displayable objects.*/
799 public boolean isLinked() {
800 if (null == hs_linked
) return false;
801 return !hs_linked
.isEmpty();
804 /** Check if this object is directly linked to a Displayable object of the given Class. */
805 public boolean isLinked(final Class c
) {
806 if (null == hs_linked
) return false;
807 for (final Displayable d
: hs_linked
) {
808 if (c
.isInstance(d
)) return true;
813 /** Check if thisobject is directly linked to the given Displayable. */
814 public boolean isLinked(final Displayable d
) {
815 if (null == hs_linked
) return false;
816 return hs_linked
.contains(d
);
819 /** Check if this object is directly linked only to Displayable objects of the given class (returns true) or to none (returns true as well).*/
820 public boolean isOnlyLinkedTo(final Class c
) {
821 if (null == hs_linked
|| hs_linked
.isEmpty()) return true;
822 for (final Displayable d
: hs_linked
) {
823 if (d
.getClass() != c
) return false;
828 /** 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.*/
829 public boolean isOnlyLinkedTo(final Class c
, final Layer layer
) {
830 if (null == hs_linked
|| hs_linked
.isEmpty()) return true;
831 for (final Displayable d
: hs_linked
) {
832 // if the class is not the asked one, or the object is not in the same layer, return false!
833 if (d
.getClass() != c
|| d
.layer
!= this.layer
) return false;
839 /** Link the Patch objects that lay underneath the bounding box of this Displayable, so that they cannot be dragged independently. */
840 public void linkPatches() {
841 final String prop
= project
.getProperty(Project
.getName(this.getClass()).toLowerCase() + "_nolinks");
842 if (null != prop
&& prop
.equals("true")) return;
843 // 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:
844 unlinkAll(Patch
.class);
846 // scan the Display and link Patch objects that lay under this Profile's bounding box:
848 // catch all displayables of the current Layer
849 final ArrayList al
= layer
.getDisplayables(Patch
.class);
851 // this bounding box:
852 final Polygon perimeter
= getPerimeter(); //displaced by this object's position!
853 if (null == perimeter
) return; // happens when a profile with zero points is deleted
855 // for each Patch, check if it underlays this profile's bounding box
856 Rectangle box
= new Rectangle();
857 for (Iterator itd
= al
.iterator(); itd
.hasNext(); ) {
858 final Displayable displ
= (Displayable
)itd
.next();
859 // 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
860 if (perimeter
.intersects(displ
.getBoundingBox(box
))) {
866 /** Unlink all Displayable objects of the given type linked by this. */
867 public void unlinkAll(final Class c
) {
868 if (!this.isLinked() || null == hs_linked
) {
871 // catch Displayables, or the iterators will go mad when deleting objects
872 final Displayable
[] displ
= new Displayable
[hs_linked
.size()];
873 hs_linked
.toArray(displ
);
874 for (int i
=0; i
<displ
.length
; i
++) {
875 if (displ
[i
].getClass() == c
) {
881 /** Check if this perimeter's intersects that of the given Displayable. */
882 public boolean intersects(final Displayable d
) {
883 return intersects(new Area(d
.getPerimeter()));
886 public boolean intersects(final Area area
) {
887 final Area a
= new Area(getPerimeter());
889 final Rectangle b
= a
.getBounds();
890 return 0 != b
.width
&& 0 != b
.height
;
893 /** Calls intersects(area) unless overriden -- intended for ZDisplayable objects to return whether they intersect the given area at the given layer. */
894 public boolean intersects(final Layer layer
, final Area area
) {
895 return intersects(area
);
898 public boolean intersects(final Layer layer
, final Rectangle r
) {
899 return getBoundingBox(null).intersects(r
);
902 /** Returns the intersection of this Displayable's area with the given one. */
903 public Area
getIntersection(final Displayable d
) {
904 final Area a
= new Area(this.getPerimeter());
905 a
.intersect(new Area(d
.getPerimeter()));
909 /** Returns the sum of bounding boxes of all linked Displayables. */
910 public Rectangle
getLinkedBox(final boolean same_layer
) {
911 if (null == hs_linked
|| hs_linked
.isEmpty()) return getBoundingBox();
912 final Rectangle box
= new Rectangle();
913 accumulateLinkedBox(same_layer
, new HashSet(), box
);
917 /** Accumulates in the box. */
918 private void accumulateLinkedBox(final boolean same_layer
, final HashSet hs_done
, final Rectangle box
) {
919 if (hs_done
.contains(this)) return;
921 box
.add(getBoundingBox(null));
922 for (final Displayable d
: hs_linked
) {
923 // add ZDisplayables regardless, for their 'layer' pointer is used to know which part of them must be painted.
924 if (same_layer
&& !(d
instanceof ZDisplayable
) && d
.layer
!= this.layer
) continue;
925 d
.accumulateLinkedBox(same_layer
, hs_done
, box
);
929 /** Minimal info that identifies this object as unique, for display on a JTree node.*/
930 public String
toString() {
931 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
934 abstract public boolean isDeletable();
936 /** Subclasses can specify the behaviour, for the default is true.*/
937 public boolean canSendTo(Layer layer
) {
941 /** Does nothing unless overriden. */
942 public void exportSVG(StringBuffer data
, double z_scale
, String indent
) {}
944 /** Does nothing unless overriden. Used for profile, pipe and ball points when preventing dragging beyond the screen, to snap to cursor when this reenters. */
945 public void snapTo(int cx
, int cy
, int x_p
, int y_p
) {}
947 /** Shows a dialog to adjust properties of this object. */
948 public void adjustProperties() {
949 GenericDialog gd
= makeAdjustPropertiesDialog();
951 if (gd
.wasCanceled()) return;
952 processAdjustPropertiesDialog(gd
);
955 protected GenericDialog
makeAdjustPropertiesDialog() {
956 Rectangle box
= getBoundingBox(null);
957 GenericDialog gd
= new GD("Properties", this);
958 gd
.addStringField("title: ", title
);
959 gd
.addNumericField("x: ", box
.x
, 2);
960 gd
.addNumericField("y: ", box
.y
, 2);
961 gd
.addNumericField("scale_x: ", 1, 2);
962 gd
.addNumericField("scale_y: ", 1, 2);
963 gd
.addNumericField("rot (degrees): ", 0, 2);
964 gd
.addSlider("alpha: ", 0, 100, (int)(alpha
*100));
965 gd
.addCheckbox("visible", visible
);
966 gd
.addSlider("Red: ", 0, 255, color
.getRed());
967 gd
.addSlider("Green: ", 0, 255, color
.getGreen());
968 gd
.addSlider("Blue: ", 0, 255, color
.getBlue());
969 gd
.addCheckbox("locked", locked
);
970 // add slider listener
971 final Scrollbar alp
= (Scrollbar
)gd
.getSliders().get(0);
972 final Scrollbar red
= (Scrollbar
)gd
.getSliders().get(1);
973 final Scrollbar green
= (Scrollbar
)gd
.getSliders().get(2);
974 final Scrollbar blue
= (Scrollbar
)gd
.getSliders().get(3);
975 final TextField talp
= (TextField
)gd
.getNumericFields().get(5);
976 final TextField tred
= (TextField
)gd
.getNumericFields().get(6);
977 final TextField tgreen
= (TextField
)gd
.getNumericFields().get(7);
978 final TextField tblue
= (TextField
)gd
.getNumericFields().get(8);
979 SliderListener sla
= new SliderListener() {
980 public void update() {
981 setAlpha((float)alp
.getValue()/100);
984 SliderListener slc
= new SliderListener() {
985 public void update() {
986 setColor(new Color(red
.getValue(), green
.getValue(), blue
.getValue()));
989 alp
.addAdjustmentListener(sla
);
990 red
.addAdjustmentListener(slc
);
991 green
.addAdjustmentListener(slc
);
992 blue
.addAdjustmentListener(slc
);
993 talp
.addTextListener(sla
);
994 tred
.addTextListener(slc
);
995 tgreen
.addTextListener(slc
);
996 tblue
.addTextListener(slc
);
1000 private abstract class SliderListener
implements AdjustmentListener
, TextListener
{
1001 public void adjustmentValueChanged(AdjustmentEvent ae
) { update(); }
1002 public void textValueChanged(TextEvent te
) { update(); }
1003 abstract public void update();
1006 private class GD
extends GenericDialog
{
1010 GD(String title
, Displayable displ
) {
1013 this.dcolor
= new Color(displ
.color
.getRed(), displ
.color
.getGreen(), displ
.color
.getBlue()); // can't clone color?
1014 this.dalpha
= displ
.alpha
;
1016 /** Override to restore original color when canceled. */
1017 public void dispose() {
1018 if (wasCanceled()) {
1019 displ
.alpha
= dalpha
;
1020 displ
.setColor(dcolor
); // calls repaint
1026 protected DoEdit
processAdjustPropertiesDialog(final GenericDialog gd
) {
1027 // store old transforms for undo
1028 HashSet
<Displayable
> hs
= getLinkedGroup(new HashSet
<Displayable
>());
1029 layer
.getParent().addTransformStep(hs
);
1031 //Rectangle box = getLinkedBox(true);//getBoundingBox();
1033 final HashSet
<String
> fields
= new HashSet
<String
>();
1036 String title1
= gd
.getNextString();
1037 double x1
= gd
.getNextNumber();
1038 double y1
= gd
.getNextNumber();
1039 double sx
= gd
.getNextNumber();
1040 double sy
= gd
.getNextNumber();
1041 double rot1
= gd
.getNextNumber();
1042 float alpha1
= (float)gd
.getNextNumber() / 100;
1044 final DoEdit prev
= new DoEdit(this);
1046 if (Double
.isNaN(x1
) || Double
.isNaN(y1
) || Double
.isNaN(sx
) || Double
.isNaN(sy
) || Float
.isNaN(alpha1
)) {
1047 Utils
.showMessage("Invalid values!");
1051 Color co
= new Color((int)gd
.getNextNumber(), (int)gd
.getNextNumber(), (int)gd
.getNextNumber());
1052 if (!co
.equals(this.color
)) {
1053 prev
.add("color", color
);
1055 updateInDatabase("color");
1057 boolean visible1
= gd
.getNextBoolean();
1058 boolean locked1
= gd
.getNextBoolean();
1059 if (!title
.equals(title1
)) {
1060 prev
.add("title", title1
);
1061 setTitle(title1
); // will update the panel
1065 // Add the transforms, even if not modified (too much pain)
1066 if (null != hs
) prev
.add(new DoTransforms().addAll(hs
));
1067 else prev
.add("at", this.getAffineTransformCopy());
1069 final Rectangle b
= getBoundingBox(null); // previous
1070 if (x1
!= b
.x
|| y1
!= b
.y
) {
1072 // apply the scaling and displacement to all linked
1073 Rectangle box_old
= getBoundingBox();
1074 // fix FreeHandProfile past errors
1075 if (0 == box_old
.width
|| 0 == box_old
.height
) {
1076 if (this instanceof Profile
) {
1077 ((Profile
)this).calculateBoundingBox(true);
1078 box_old
= getBoundingBox();
1080 //avoid division by zero
1081 Utils
.showMessage("Some error ocurred: zero width or height ob the object to adjust.\nUnlink this object '" + this + "' and adjust carefully");
1085 this.setLocation(x1
, y1
);
1086 Rectangle b2
= getBoundingBox(null);
1087 int dx
= b2
.x
- b
.x
;
1088 int dy
= b2
.y
- b
.y
;
1089 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1090 Displayable d
= (Displayable
)it
.next();
1091 if (this.equals(d
)) continue;
1092 d
.translate(dx
, dy
, false);
1095 this.setLocation(x1
, y1
);
1098 if (1 != sx
|| 1 != sy
) {
1101 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1102 Displayable d
= (Displayable
)it
.next();
1103 d
.scale(sx
, sy
, b
.y
+b
.width
/2, b
.y
+b
.height
/2, false); // centered on this
1106 this.scale(sx
, sy
, b
.y
+b
.width
/2, b
.y
+b
.height
/2, false);
1110 // rotate relative to the center of he Displayable
1111 double rads
= Math
.toRadians(rot1
);
1113 //Utils.log2("delta_angle, rot1, rot: " + delta_angle + "," + rot1 + "," + rot);
1114 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1115 Displayable d
= (Displayable
)it
.next();
1116 d
.rotate(rads
, b
.x
+b
.width
/2, b
.y
+b
.height
/2, false);
1119 this.rotate(rads
, b
.x
+b
.width
/2, b
.y
+b
.height
/2, false);
1122 if (alpha1
!= alpha
) {
1123 prev
.add("alpha", alpha1
);
1124 setAlpha(alpha1
, true);
1126 if (visible1
!= visible
) {
1127 prev
.add("visible", visible1
);
1128 setVisible(visible1
);
1130 if (locked1
!= locked
) {
1131 prev
.add("locked", locked1
);
1135 getLayerSet().addEditStep(prev
); // contains the transformations of all others, if necessary.
1137 // it's lengthy to predict the precise box for each open Display, so just repaint all in all Displays.
1138 Display
.updateSelection();
1139 Display
.repaint(getLayer()); // not this.layer, so ZDisplayables are repainted properly
1144 static protected final String TAG_ATTR1
= "<!ATTLIST ";
1145 static protected final String TAG_ATTR2
= " NMTOKEN #REQUIRED>\n";
1147 /** Adds simply DTD !ATTRIBUTE tags. The ProjectThing that encapsulates this object will give the type. */
1148 static public void exportDTD(String type
, StringBuffer sb_header
, HashSet hs
, String indent
) {
1149 sb_header
.append(indent
).append(TAG_ATTR1
).append(type
).append(" oid").append(TAG_ATTR2
)
1150 .append(indent
).append(TAG_ATTR1
).append(type
).append(" layer_id").append(TAG_ATTR2
)
1152 .append(indent).append(TAG_ATTR1).append(type).append(" x").append(TAG_ATTR2)
1153 .append(indent).append(TAG_ATTR1).append(type).append(" y").append(TAG_ATTR2)
1154 .append(indent).append(TAG_ATTR1).append(type).append(" width").append(TAG_ATTR2)
1155 .append(indent).append(TAG_ATTR1).append(type).append(" height").append(TAG_ATTR2)
1157 .append(indent
).append(TAG_ATTR1
).append(type
).append(" transform").append(TAG_ATTR2
)
1158 .append(indent
).append(TAG_ATTR1
).append(type
).append(" style").append(TAG_ATTR2
)
1159 .append(indent
).append(TAG_ATTR1
).append(type
).append(" locked").append(TAG_ATTR2
)
1160 .append(indent
).append(TAG_ATTR1
).append(type
).append(" visible").append(TAG_ATTR2
)
1161 .append(indent
).append(TAG_ATTR1
).append(type
).append(" title").append(TAG_ATTR2
)
1162 .append(indent
).append(TAG_ATTR1
).append(type
).append(" links").append(TAG_ATTR2
)
1166 static public void exportDTD(StringBuffer sb_header
, HashSet hs
, String indent
) {
1167 if (!hs
.contains("t2_prop")) {
1168 sb_header
.append(indent
).append("<!ELEMENT t2_prop EMPTY>\n")
1169 .append(indent
).append(TAG_ATTR1
).append("t2_prop key").append(TAG_ATTR2
)
1170 .append(indent
).append(TAG_ATTR1
).append("t2_prop value").append(TAG_ATTR2
)
1173 if (!hs
.contains("t2_linked_prop")) {
1174 sb_header
.append(indent
).append("<!ELEMENT t2_linked_prop EMPTY>\n")
1175 .append(indent
).append(TAG_ATTR1
).append("t2_linked_prop target_id").append(TAG_ATTR2
)
1176 .append(indent
).append(TAG_ATTR1
).append("t2_linked_prop key").append(TAG_ATTR2
)
1177 .append(indent
).append(TAG_ATTR1
).append("t2_linked_prop value").append(TAG_ATTR2
)
1182 static protected String
commonDTDChildren() {
1183 return "t2_prop,t2_linked_prop"; // never commas at beginning or end, only in between
1184 // never returns empty
1187 /** 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
1188 public void exportXML(final StringBuffer sb_body
, final String in
, final Object any
) {
1189 final double[] a
= new double[6];
1191 sb_body
.append(in
).append("oid=\"").append(id
).append("\"\n")
1192 .append(in
).append("width=\"").append(width
).append("\"\n")
1193 .append(in
).append("height=\"").append(height
).append("\"\n")
1194 .append(in
).append("transform=\"matrix(").append(a
[0]).append(',')
1195 .append(a
[1]).append(',')
1196 .append(a
[2]).append(',')
1197 .append(a
[3]).append(',')
1198 .append(a
[4]).append(',')
1199 .append(a
[5]).append(")\"\n")
1201 // the default is obvious, so just store the value if necessary
1202 if (locked
) sb_body
.append(in
).append("locked=\"true\"\n");
1203 if (!visible
) sb_body
.append(in
).append("visible=\"false\"\n");
1204 // 'style' is taken care in subclasses
1205 if (null != title
&& title
.length() > 0) {
1206 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)
1208 sb_body
.append(in
).append("links=\"");
1209 if (null != hs_linked
&& 0 != hs_linked
.size()) {
1210 // Sort the ids: so resaving the file saves an identical file (otherwise, ids are in different order).
1211 final long[] ids
= new long[hs_linked
.size()];
1213 for (final Displayable d
: hs_linked
) ids
[ii
++] = d
.id
;
1215 for (int g
=0; g
<ids
.length
; g
++) sb_body
.append(ids
[g
]).append(',');
1216 sb_body
.setLength(sb_body
.length()-1); // remove last comma by shifting cursor backwards
1218 sb_body
.append("\"\n");
1221 /** Add properties, links, etc. Does NOT close the tag. */
1222 synchronized protected void restXML(final StringBuffer sb_body
, final String in
, final Object any
) {
1224 if (null != props
&& !props
.isEmpty()) {
1225 for (final Map
.Entry
<String
,String
> e
: props
.entrySet()) {
1226 final String value
= e
.getValue();
1227 if (null == value
) continue; // impossible, but with reflection one may set it so
1228 sb_body
.append(in
).append("<t2_prop key=\"").append(e
.getKey()).append("\" value=\"").append(cleanAttr(e
, value
)).append("\" />\n");
1231 if (null != linked_props
&& !linked_props
.isEmpty()) {
1232 for (final Map
.Entry
<Displayable
,Map
<String
,String
>> et
: linked_props
.entrySet()) {
1233 final Displayable target
= et
.getKey();
1234 for (final Map
.Entry
<String
,String
> e
: et
.getValue().entrySet()) {
1235 final String value
= e
.getValue();
1236 if (null == value
) continue; // impossible, but with reflection one may set it so
1237 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");
1243 // Make sure the value is valid for an XML attribute content inside double quotes.
1244 final private String
cleanAttr(final Map
.Entry
<String
,String
> e
, String value
) {
1245 if (-1 != value
.indexOf('"')) {
1246 Utils
.log("Property " + e
.getKey() + " for ob id=#" + this.id
+ " contains a \" which is being replaced by '.");
1247 value
= value
.replace('"', '\'');
1249 if (-1 != value
.indexOf('\n')) {
1250 Utils
.log("Property " + e
.getKey() + " for ob id=#" + this.id
+ " contains a newline char which is being replaced by a space.");
1251 value
= value
.replace('\n', ' ');
1256 // I'm sure it could be made more efficient, but I'm too tired!
1257 public boolean hasLinkedGroupWithinLayer(Layer la
) {
1258 HashSet hs
= getLinkedGroup(new HashSet());
1259 for (Iterator it
= hs
.iterator(); it
.hasNext(); ) {
1260 Displayable d
= (Displayable
)it
.next();
1261 if (!d
.layer
.equals(la
)) return false;
1266 /** Rotate 2D points relative to the given pivot point by the given rot angle (in radians), in the 2D plane. */
1267 static public double[][] rotatePoints(double[][] p
, double rot
, double xo
, double yo
) {
1268 //Utils.log("calling rotatePoints for " + p + " with rot=" + Math.toDegrees(rot));
1270 Utils
.log2("WARNING: Displayable.rotatePoints received a null points array.");
1271 return new double[0][0];
1273 int length
= p
[0].length
;
1274 double[][] pr
= new double[p
.length
][length
];
1275 for (int i
=0; i
<length
; i
++) {
1276 // catch for readability
1279 // angle relative to the pivot point
1280 double b1
= Utils
.getAngle(x
- xo
, y
- yo
);
1281 // new angle relative to pivot point
1282 double b2
= b1
+ rot
;
1284 double hypot
= Math
.sqrt((x
- xo
)*(x
- xo
) + (y
- yo
)*(y
- yo
));
1285 pr
[0][i
] = xo
+ (Math
.cos(b2
) * hypot
); //x + (Math.cos(b2) - Math.cos(b1)) * hypot;
1286 pr
[1][i
] = yo
+ (Math
.sin(b2
) * hypot
); //y + (Math.sin(b2) - Math.sin(b1)) * hypot;
1291 /** Scale 2D points relative to the given pivot point xo,yo. */
1292 static public double[][] scalePoints(double[][] p
, double sx
, double sy
, double xo
, double yo
) {
1293 int length
= p
[0].length
;
1294 double[][] ps
= new double[p
.length
][length
];
1295 for (int i
=0; i
<length
; i
++) {
1296 ps
[0][i
] = xo
+ (p
[0][i
] - xo
) * sx
;
1297 ps
[1][i
] = yo
+ (p
[1][i
] - yo
) * sy
;
1302 static public double[][] displacePoints(double[][] p
, double dx
, double dy
) {
1303 int length
= p
[0].length
;
1304 double[][] pd
= new double[p
.length
][length
];
1305 for (int i
=0; i
<length
; i
++) {
1306 pd
[0][i
] = p
[0][i
] + dx
;
1307 pd
[1][i
] = p
[1][i
] + dy
;
1312 /** Transform in place only the 'i' point in the points array.*/
1313 static public void transformPoint(double[][] p
, int i
, double dx
, double dy
, double rot
, double xo
, double yo
) {
1317 double hypot
= Math
.sqrt(Math
.pow(p
[0][i
] - xo
, 2) + Math
.pow(p
[1][i
] - yo
, 2));
1318 double angle
= Utils
.getAngle(p
[0][i
] - xo
, p
[1][i
] - yo
);
1319 p
[0][i
] = xo
+ Math
.cos(angle
+ rot
) * hypot
;
1320 p
[1][i
] = yo
+ Math
.sin(angle
+ rot
) * hypot
;
1324 /** Fine nearest point in array a, from 0 up to n_points, to point x_p,y_p.
1325 * @return the index of such point. */
1326 static protected int findNearestPoint(final double[][] a
, final int n_points
, final double x_p
, final double y_p
) {
1327 if (0 == n_points
) return -1;
1328 double min_dist
= Double
.MAX_VALUE
;
1330 for (int i
=0; i
<n_points
; i
++) {
1331 double sq_dist
= Math
.pow(a
[0][i
] - x_p
, 2) + Math
.pow(a
[1][i
] - y_p
, 2);
1332 if (sq_dist
< min_dist
) {
1340 /** Performs a deep copy of this object. */
1341 public Displayable
clone() {
1342 return clone(this.project
);
1345 /** 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. */
1346 abstract public Displayable
clone(Project pr
, boolean copy_id
);
1348 /** Performs a deep copy of this object but assigning to it the given project. The visibility, though, is set to true at all times. */
1349 public Displayable
clone(Project pr
) {
1350 return clone(pr
, false);
1353 public LayerSet
getLayerSet() {
1354 if (null != layer
) return layer
.getParent();
1358 public boolean updateInDatabase(String key
) {
1359 // ???? TODO ???? cruft from the past? // project.getLoader().updateCache(this, key);
1360 //if (Utils.java3d) Display3D.update(this);
1361 return super.updateInDatabase(key
);
1364 static public Rectangle
getMinimalBoundingBox(Displayable
[] d
) {
1365 final Rectangle box
= d
[0].getBoundingBox();
1366 final Rectangle tmp
= new Rectangle();
1367 for (int i
=1; i
<d
.length
; i
++) {
1368 box
.add(d
[i
].getBoundingBox(tmp
));
1373 public AffineTransform
getAffineTransform() {
1377 public AffineTransform
getAffineTransformCopy() {
1378 return (AffineTransform
)at
.clone();
1381 /** Sets the matrix values of this Displayable's AffineTransform to those of the given AffineTransform. */
1382 public void setAffineTransform(AffineTransform at
) {
1383 this.at
.setTransform(at
);
1384 updateInDatabase("transform");
1388 /** Translate this Displayable and its linked ones if linked=true. */
1389 public void translate(double dx
, double dy
, boolean linked
) {
1390 if (Double
.isNaN(dx
) || Double
.isNaN(dy
)) return;
1391 final AffineTransform at2
= new AffineTransform();
1392 at2
.translate(dx
, dy
);
1393 preTransform(at2
, linked
);
1396 public void translate(double dx
, double dy
) {
1397 translate(dx
, dy
, true);
1400 /** Rotate relative to an anchor point. */
1401 public void rotate(double radians
, double xo
, double yo
) {
1402 rotate(radians
, xo
, yo
, true);
1405 /** Rotate relative to an anchor point. */
1406 public void rotate(double radians
, double xo
, double yo
, boolean linked
) {
1407 if (Double
.isNaN(radians
) || Double
.isNaN(xo
) || Double
.isNaN(yo
)) return;
1408 final AffineTransform at2
= new AffineTransform();
1409 at2
.rotate(radians
, xo
, yo
);
1410 preTransform(at2
, linked
);
1413 /** Commands the parent container (a Layer or a LayerSet) to update the bucket position of this Displayable. */
1414 public void updateBucket() {
1415 if (null != getBucketable()) getBucketable().updateBucket(this);
1418 /** Scale relative to an anchor point (will translate as necessary). */
1419 public void scale(double sx
, double sy
, double xo
, double yo
) {
1420 scale(sx
, sy
, xo
, yo
, true);
1423 /** Scale relative to an anchor point (will translate as necessary). */
1424 public void scale(double sx
, double sy
, double xo
, double yo
, boolean linked
) {
1425 if (Double
.isNaN(sx
) || Double
.isNaN(sy
) || Double
.isNaN(xo
) || Double
.isNaN(yo
)) return;
1426 final AffineTransform at2
= new AffineTransform();
1427 at2
.translate( xo
, yo
);
1428 at2
.scale( sx
, sy
);
1429 at2
.translate( -xo
, -yo
);
1430 preTransform(at2
, linked
);
1433 /** 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. */
1434 public void setLocation(double x
, double y
) {
1435 if (Double
.isNaN(x
) || Double
.isNaN(y
)) return;
1436 Rectangle b
= getBoundingBox(null);
1437 this.translate(x
- b
.x
, y
- b
.y
, false); // do not affect linked Displayables
1438 //Utils.log2("setting new loc, args are: " + x + ", "+ y);
1442 /** Apply this Displayable's AffineTransform to the given point. */
1443 public Point2D
.Double
transformPoint(final double px
, final double py
) {
1444 final Point2D
.Double pSrc
= new Point2D
.Double(px
, py
);
1445 if (this.at
.isIdentity()) return pSrc
;
1446 final Point2D
.Double pDst
= new Point2D
.Double();
1447 this.at
.transform(pSrc
, pDst
);
1451 public Point2D
.Double
inverseTransformPoint(final double px
, final double py
) {
1452 final Point2D
.Double pSrc
= new Point2D
.Double(px
, py
);
1453 if (this.at
.isIdentity()) return pSrc
;
1454 final Point2D
.Double pDst
= new Point2D
.Double();
1456 //this.at.createInverse().transform(pSrc, pDst);
1457 this.at
.inverseTransform(pSrc
, pDst
);
1458 } catch (NoninvertibleTransformException nite
) {
1459 IJError
.print(nite
);
1464 /** 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.*/
1465 final public Rectangle
transformRectangle(final Rectangle r
) {
1466 if (this.at
.isIdentity()) return (Rectangle
)r
.clone();
1467 return new Area(r
).createTransformedArea(this.at
).getBounds();
1470 /** 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.
1472 public double[][] transformPoints(final double[][] p
) {
1473 return transformPoints(p
, p
[0].length
);
1476 /** Will crop second dimension of the given array at the given length. */
1477 protected double[][] transformPoints(final double[][] p
, final int length
) {
1478 if (this.at
.isIdentity()) return p
;
1479 //final int length = p[0].length;
1480 final double[] p2a
= new double[length
* 2];
1481 for (int i
=0, j
=0; i
<length
; i
++, j
+=2) {
1485 final double[] p2b
= new double[length
* 2];
1486 this.at
.transform(p2a
, 0, p2b
, 0, length
); // what a silly format: consecutive x,y numbers! Clear case of premature optimization.
1487 final double[][] p3
= new double[2][length
];
1488 for (int i
=0, j
=0; i
<length
; i
++, j
+=2) {
1490 p3
[1][i
] = p2b
[j
+1];
1495 /** Concatenate the given affine to this and all its linked objects. */
1496 public void transform(final AffineTransform at
) {
1497 for (Iterator it
= getLinkedGroup(new HashSet()).iterator(); it
.hasNext(); ) {
1498 Displayable d
= (Displayable
)it
.next();
1499 d
.at
.concatenate(at
);
1500 d
.updateInDatabase("transform");
1502 //Utils.log("applying transform to " + d);
1506 /** preConcatenate the given affine transform to this Displayable's affine. */
1507 public void preTransform(final AffineTransform affine
, final boolean linked
) {
1509 for (Iterator it
= getLinkedGroup(null).iterator(); it
.hasNext(); ) {
1510 final Displayable d
= (Displayable
)it
.next();
1511 d
.at
.preConcatenate(affine
);
1512 d
.updateInDatabase("transform");
1516 this.at
.preConcatenate(affine
);
1517 this.updateInDatabase("transform");
1518 this.updateBucket();
1522 public void paintAsBox(final Graphics2D g
) {
1523 final double[] c
= new double[]{0,0, width
,0, width
,height
, 0,height
};
1524 final double[] c2
= new double[8];
1525 this.at
.transform(c
, 0, c2
, 0, 4);
1527 g
.drawLine((int)c2
[0], (int)c2
[1], (int)c2
[2], (int)c2
[3]);
1528 g
.drawLine((int)c2
[2], (int)c2
[3], (int)c2
[4], (int)c2
[5]);
1529 g
.drawLine((int)c2
[4], (int)c2
[5], (int)c2
[6], (int)c2
[7]);
1530 g
.drawLine((int)c2
[6], (int)c2
[7], (int)c2
[0], (int)c2
[1]);
1533 public void paintSnapshot(final Graphics2D g
, final double mag
) {
1534 switch (layer
.getParent().getSnapshotsMode()) {
1536 paint(g
, mag
, false, 0xffffffff, layer
);
1541 default: return; // case 2: // disabled, no paint
1545 public DBObject
findById(final long id
) {
1546 if (this.id
== id
) return this;
1550 /** Does nothing unless overriden. */
1551 public ResultsTable
measure(ResultsTable rt
) {
1552 Utils
.showMessage("Not implemented yet for " + Project
.getName(getClass()) + " [class " + this.getClass().getName() + "]");
1556 public Bucketable
getBucketable() {
1560 /** If the title is purely numeric, returns it as a double; otherwise returns 0. */
1561 protected double getNameId() {
1563 if (null != this.title
) {
1565 nameid
= Double
.parseDouble(this.title
.trim());
1566 } catch (NumberFormatException nfe
) {}
1573 class DoEdits
implements DoStep
{
1574 final HashSet
<DoEdit
> edits
= new HashSet
<DoEdit
>();
1575 DoEdits(final Set
<Displayable
> col
) {
1576 for (final Displayable d
: col
) {
1577 edits
.add(new DoEdit(d
));
1580 public Displayable
getD() { return null; }
1581 public boolean isIdenticalTo(final Object ob
) {
1582 if (!(ob
instanceof DoEdits
)) return false;
1583 final DoEdits other
= (DoEdits
) ob
;
1584 if (this.edits
.size() != other
.edits
.size()) return false;
1585 final Iterator
<DoEdit
> it1
= this.edits
.iterator();
1586 final Iterator
<DoEdit
> it2
= other
.edits
.iterator();
1587 // Order matters: (but it shouldn't!) TODO
1588 for (; it1
.hasNext() && it2
.hasNext(); ) {
1589 if ( ! it1
.next().isIdenticalTo(it2
.next())) return false;
1593 public void init(final String
[] fields
) {
1594 for (final DoEdit edit
: edits
) {
1595 edit
.init(edit
.d
, fields
);
1598 public boolean isEmpty() { return edits
.isEmpty(); }
1599 public boolean apply(int action
) {
1600 boolean failed
= false;
1601 for (final DoEdit edit
: edits
) {
1602 if (!edit
.apply(action
)) {
1610 /** For any Displayable data, including: title, visible, locked, color, alpha,
1611 * 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).*/
1612 class DoEdit
implements DoStep
{
1613 private final HashMap
<String
,Object
> content
= new HashMap
<String
,Object
>();
1614 private ArrayList
<DoStep
> dependents
= null;
1615 private final Displayable d
;
1616 /** Returns self on success, otherwise null. */
1617 DoEdit(final Displayable d
) {
1620 public boolean containsKey(final String field
) {
1621 return content
.containsKey(field
);
1623 public boolean isIdenticalTo(final Object ob
) {
1624 if (!(ob
instanceof DoEdit
)) return false;
1625 final DoEdit other
= (DoEdit
) ob
;
1626 // same Displayable?
1627 if (this.d
!= other
.d
) return false;
1629 if (null != dependents
) {
1633 // same number of fields to edit?
1634 if (this.content
.size() != other
.content
.size()) return false;
1635 // any data? Currently comparisons of data are disabled
1636 if (null != this.content
.get("data") || null != other
.content
.get("data")) {
1639 // same content of fields?
1640 for (final Map
.Entry
<String
,Object
> e
: this.content
.entrySet()) {
1641 final Object val
= other
.content
.get(e
.getKey());
1643 Utils
.log2("WARNING: null val for " + e
.getKey());
1646 if (val
instanceof HashMap
) {
1647 if ( ! identical((HashMap
)val
, (HashMap
)e
.getValue())) {
1650 } else if ( ! val
.equals(e
.getValue())) return false;
1654 private boolean identical(final HashMap m1
, final HashMap m2
) {
1655 if (m1
.size() != m2
.size()) return false;
1656 for (final Object entry
: m1
.entrySet()) {
1657 final Map
.Entry e
= (Map
.Entry
) entry
;
1658 // TODO this could fail if value is null
1659 if ( ! e
.getValue().equals(m2
.get(e
.getKey()))) return false;
1663 synchronized public Displayable
getD() { return d
; }
1664 synchronized DoEdit
fullCopy() {
1665 return init(d
, new String
[]{"data", "width", "height", "locked", "title", "color", "alpha", "visible", "props", "linked_props"});
1667 /** With the same keys as 'de'. */
1668 synchronized DoEdit
init(final DoEdit de
) {
1669 return init(de
.d
, de
.content
.keySet().toArray(new String
[0]));
1671 synchronized public boolean add(final DoStep step
) {
1672 if (null == dependents
) dependents
= new ArrayList
<DoStep
>();
1673 if (dependents
.contains(step
)) return false;
1674 dependents
.add(step
);
1677 synchronized public boolean add(final String field
, final Object value
) {
1678 content
.put(field
, value
);
1681 synchronized DoEdit
init(final Displayable d
, final String
[] fields
) {
1682 final Class
[] c
= new Class
[]{Displayable
.class, d
.getClass(), ZDisplayable
.class};
1683 for (int k
=0; k
<fields
.length
; k
++) {
1684 if ("data".equals(fields
[k
])) {
1685 content
.put(fields
[k
], d
.getDataPackage());
1687 boolean got_it
= false;
1688 for (int i
=0; i
<c
.length
; i
++) {
1690 java
.lang
.reflect
.Field f
= c
[i
].getDeclaredField(fields
[k
]);
1691 if (null == f
) continue; // will throw a NoSuchFieldException, but just in case
1692 f
.setAccessible(true);
1693 Object ob
= f
.get(d
);
1694 content
.put(fields
[k
], null != ob ?
duplicate(ob
, fields
[k
]) : null);
1696 } catch (NoSuchFieldException nsfe
) {
1697 } catch (IllegalAccessException iae
) {}
1700 Utils
.log2("ERROR: could not get '" + fields
[k
] + "' field for " + d
);
1707 /** 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 @#$%!
1708 private final Object
duplicate(final Object ob
, final String field
) {
1709 if (ob
instanceof Color
) {
1710 final Color c
= (Color
)ob
;
1711 return new Color(c
.getRed(), c
.getGreen(), c
.getBlue());
1712 } else if (ob
instanceof HashMap
) {
1713 if (field
.equals("linked_props")) {
1714 final HashMap hm
= new HashMap();
1715 for (final Object e
: ((HashMap
)ob
).entrySet()) {
1716 final Map
.Entry me
= (Map
.Entry
)e
;
1717 hm
.put(me
.getKey(), ((HashMap
)me
.getValue()).clone());
1721 return new HashMap((HashMap
)ob
);
1723 // Number, Boolean, String are all final classes:
1726 /** Set the stored data to the stored Displayable. */
1727 public boolean apply(int action
) {
1728 final Class
[] c
= new Class
[]{Displayable
.class, d
.getClass(), ZDisplayable
.class};
1729 for (final Map
.Entry
<String
,Object
> e
: content
.entrySet()) {
1730 String field
= e
.getKey();
1731 if ("data".equals(field
)) {
1732 if (!d
.setDataPackage((DataPackage
)e
.getValue())) {
1737 for (int i
=0; i
<c
.length
; i
++) {
1738 java
.lang
.reflect
.Field f
= c
[i
].getDeclaredField(field
);
1739 f
.setAccessible(true);
1740 f
.set(d
, e
.getValue());
1742 } catch (NoSuchFieldException nsfe
) {
1743 } catch (IllegalAccessException iae
) {
1744 } catch (Exception ex
) {
1751 if (null != dependents
) {
1752 for (final DoStep step
: dependents
) {
1753 if (!step
.apply(action
)) ok
= false;
1756 Display
.update(d
.getLayerSet());
1759 public boolean isEmpty() {
1760 return null == d
|| (content
.isEmpty() && (null == dependents
|| dependents
.isEmpty()));
1764 protected class DoTransforms
implements DoStep
{
1765 final private HashMap
<Displayable
,AffineTransform
> ht
= new HashMap
<Displayable
,AffineTransform
>();
1766 final HashSet
<Layer
> layers
= new HashSet
<Layer
>();
1768 DoTransforms
addAll(final Collection
<Displayable
> col
) {
1769 for (final Displayable d
: col
) {
1770 ht
.put(d
, d
.getAffineTransformCopy());
1771 layers
.add(d
.getLayer());
1775 public boolean isEmpty() {
1776 return null == ht
|| ht
.isEmpty();
1778 public boolean apply(int action
) {
1779 if (isEmpty()) return false;
1780 for (final Map
.Entry
<Displayable
,AffineTransform
> e
: ht
.entrySet()) {
1781 e
.getKey().at
.setTransform(e
.getValue());
1783 for (final Layer layer
: layers
) {
1784 layer
.recreateBuckets();
1786 if (!layers
.isEmpty()) layers
.iterator().next().getParent().recreateBuckets(false);
1789 public Displayable
getD() { return null; }
1791 public boolean isIdenticalTo(final Object ob
) {
1792 if (ob
instanceof Collection
) {
1793 final Collection
<Displayable
> col
= (Collection
<Displayable
>) ob
;
1794 if (ht
.size() != col
.size()) return false;
1795 for (final Displayable d
: col
) {
1796 if (!d
.getAffineTransform().equals(ht
.get(d
))) {
1801 } else if (ob
instanceof DoTransforms
) {
1802 final DoTransforms dt
= (DoTransforms
) ob
;
1803 if (dt
.ht
.size() != this.ht
.size()) return false;
1804 for (final Map
.Entry
<Displayable
,AffineTransform
> e
: this.ht
.entrySet()) {
1805 if ( ! e
.getValue().equals(dt
.ht
.get(e
.getKey()))) {
1815 synchronized final boolean setDataPackage(final Displayable
.DataPackage pkg
) {
1816 if (pkg
.getClass() != getInternalDataPackageClass()) {
1817 Utils
.log2("ERROR: cannot set " + pkg
.getClass() + " to " + this.getClass());
1821 return pkg
.to2(this);
1823 } catch (Exception e
) {
1829 // Must be overriden by subclasses
1830 Object
getDataPackage() {
1831 Utils
.log2("Displayable.getDataPackage not implemented yet for " + getClass());
1834 // Must be overriden by subclasses
1835 Class
getInternalDataPackageClass() {
1836 return DataPackage
.class;
1839 static abstract protected class DataPackage
{
1840 protected final double width
, height
;
1841 protected final AffineTransform at
;
1842 protected HashMap
<Displayable
,HashSet
<Displayable
>> links
= null;
1844 DataPackage(final Displayable d
) {
1845 this.width
= d
.width
;
1846 this.height
= d
.height
;
1847 this.at
= new AffineTransform(d
.at
);
1848 if (null != d
.hs_linked
) {
1849 this.links
= new HashMap
<Displayable
,HashSet
<Displayable
>>();
1850 for (final Displayable ln
: d
.hs_linked
) {
1851 this.links
.put(ln
, new HashSet
<Displayable
>(ln
.hs_linked
));
1854 this.links
.put(d
, new HashSet
<Displayable
>(d
.hs_linked
));
1858 /** Set the Displayable's fields. */
1859 final boolean to1(final Displayable d
) {
1860 Utils
.log2("## to1");
1863 d
.setAffineTransform(at
); // updates bucket
1864 if (null != links
) {
1865 for (final Map
.Entry
<Displayable
,HashSet
<Displayable
>> e
: links
.entrySet()) {
1866 e
.getKey().hs_linked
= new HashSet
<Displayable
>(e
.getValue());
1867 Utils
.log2("setting links to " + d
);
1872 // Could simply use a single 'to' method (it works, tested),
1873 // but if I ever was to cast inadvertendly to Displayable, then
1874 // only the superclass' 'to' method would be invoked, not the
1875 // subclass' one! I call it "defensive programming"
1876 /** Set the subclass specific data fields. */
1877 abstract boolean to2(final Displayable d
);
1880 /** Returns true if any Displayable objects of different layers in sublist are linked to each other.
1881 * If ignore_stacks is true, then image links across layers are ignored. */
1882 static public final boolean areThereLayerCrossLinks(final Set
<Layer
> sublist
, final boolean ignore_stacks
) {
1883 if (null == sublist
|| 0 == sublist
.size()) return false;
1884 for (final Layer l
: sublist
) {
1885 for (final Displayable d
: l
.getDisplayables(Patch
.class)) {
1887 for (final Displayable other
: d
.getLinked()) {
1888 final Class c
= other
.getClass();
1889 if ( (!ignore_stacks
&& Patch
.class == c
&& other
.layer
!= d
.layer
)
1890 || (Profile
.class == c
&& other
.getLinked(Profile
.class).size() > 0)
1891 || ZDisplayable
.class.isAssignableFrom(c
)) {