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
;
25 import ij
.measure
.Calibration
;
26 import ij
.measure
.ResultsTable
;
28 import ini
.trakem2
.Project
;
29 import ini
.trakem2
.utils
.IJError
;
30 import ini
.trakem2
.utils
.ProjectToolbar
;
31 import ini
.trakem2
.utils
.Utils
;
32 import ini
.trakem2
.utils
.Search
;
33 import ini
.trakem2
.persistence
.DBObject
;
36 import java
.awt
.event
.MouseEvent
;
37 import java
.awt
.event
.KeyEvent
;
38 import java
.util
.ArrayList
;
40 import java
.util
.Iterator
;
41 import java
.util
.HashMap
;
42 import java
.util
.HashSet
;
43 import java
.util
.List
;
44 import java
.awt
.geom
.AffineTransform
;
45 import java
.awt
.geom
.Point2D
;
46 import java
.awt
.geom
.Area
;
48 import javax
.vecmath
.Point3f
;
50 public class Ball
extends ZDisplayable
{
52 /**The number of points.*/
53 protected int n_points
;
54 /**The array of clicked points.*/
55 protected double[][] p
;
56 /**The array of Layers over which each point lives */
57 protected long[] p_layer
;
58 /**The width of each point. */
59 protected double[] p_width
;
61 /** Every new Ball will have, for its first point, the last user-adjusted radius value or the radius of the last user-selected point. */
62 static private double last_radius
= -1;
64 public Ball(Project project
, String title
, double x
, double y
) {
65 super(project
, title
, x
, y
);
68 p_layer
= new long[5]; // the ids of the layers in which each point lays
69 p_width
= new double[5];
73 /** Construct an unloaded Ball from the database. Points will be loaded later, when needed. */
74 public Ball(Project project
, long id
, String title
, double width
, double height
, float alpha
, boolean visible
, Color color
, boolean locked
, AffineTransform at
) {
75 super(project
, id
, title
, locked
, at
, width
, height
);
76 this.visible
= visible
;
79 this.n_points
= -1; //used as a flag to signal "I have points, but unloaded"
82 /** Construct a Ball from an XML entry. */
83 public Ball(Project project
, long id
, HashMap ht
, HashMap ht_links
) {
84 super(project
, id
, ht
, ht_links
);
85 // indivudal balls will be added as soon as parsed
87 this.p
= new double[2][5];
88 this.p_layer
= new long[5];
89 this.p_width
= new double[5];
92 /** Used to add individual ball objects when parsing. */
93 public void addBall(double x
, double y
, double r
, long layer_id
) {
94 if (p
[0].length
== n_points
) enlargeArrays();
97 p_width
[n_points
] = r
;
98 p_layer
[n_points
] = layer_id
;
102 /**Increase the size of the arrays by 5.*/
103 private void enlargeArrays() {
105 int length
= p
[0].length
;
107 double[][] p_copy
= new double[2][length
+ 5];
108 long[] p_layer_copy
= new long[length
+ 5];
109 double[] p_width_copy
= new double[length
+ 5];
111 System
.arraycopy(p
[0], 0, p_copy
[0], 0, length
);
112 System
.arraycopy(p
[1], 0, p_copy
[1], 0, length
);
113 System
.arraycopy(p_layer
, 0, p_layer_copy
, 0, length
);
114 System
.arraycopy(p_width
, 0, p_width_copy
, 0, length
);
117 this.p_layer
= p_layer_copy
;
118 this.p_width
= p_width_copy
;
121 /**Find a point in an array, with a precision dependent on the magnification.*/
122 protected int findPoint(double[][] a
, int x_p
, int y_p
, double magnification
) {
124 double d
= (10.0D
/ magnification
);
126 for (int i
=0; i
<n_points
; i
++) {
127 if ((Math
.abs(x_p
- a
[0][i
]) + Math
.abs(y_p
- a
[1][i
])) <= p_width
[i
]) {
134 protected void removePoint(int index
) {
135 // check preconditions:
138 } else if (n_points
- 1 == index
) {
142 //one point out (but not the last)
145 // shift all points after 'index' one position to the left:
146 for (int i
=index
; i
<n_points
; i
++) {
147 p
[0][i
] = p
[0][i
+1]; //the +1 doesn't fail ever because the n_points has been adjusted above, but the arrays are still the same size. The case of deleting the last point is taken care above.
149 p_layer
[i
] = p_layer
[i
+1];
150 p_width
[i
] = p_width
[i
+1];
154 //later! Otherwise can't repaint properly//calculateBoundingBox(true);
157 updateInDatabase("points");
160 /**Move backbone point by the given deltas.*/
161 private void dragPoint(int index
, int dx
, int dy
) {
166 static private double getFirstWidth() {
167 if (null == Display
.getFront()) return 10;
168 if (-1 != last_radius
) return last_radius
;
169 return 10 / Display
.getFront().getCanvas().getMagnification(); // 10 pixels in the screen
172 /**Add a point either at the end or between two existing points, with accuracy depending on magnification. The width of the new point is that of the closest point after which it is inserted.*/
173 protected int addPoint(int x_p
, int y_p
, double magnification
, long layer_id
) {
174 if (-1 == n_points
) setupForDisplay(); //reload
176 if (p
[0].length
== n_points
) {
180 p
[0][n_points
] = x_p
;
181 p
[1][n_points
] = y_p
;
182 p_layer
[n_points
] = layer_id
;
183 p_width
[n_points
] = (0 == n_points ? Ball
.getFirstWidth() : p_width
[n_points
-1]); // either 10 screen pixels or the same as the last point
187 updateInDatabase(new StringBuffer("INSERT INTO ab_ball_points (ball_id, x, y, width, layer_id) VALUES (").append(id
).append(",").append(x_p
).append(",").append(y_p
).append(",").append(p_width
[index
]).append(",").append(layer_id
).append(")").toString());
191 public void paint(final Graphics2D g
, final double magnification
, final boolean active
, final int channels
, final Layer active_layer
) {
192 if (0 == n_points
) return;
193 if (-1 == n_points
) {
194 // load points from the database
197 //arrange transparency
198 Composite original_composite
= null;
200 original_composite
= g
.getComposite();
201 g
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, alpha
));
204 // local pointers, since they may be transformed
205 double[][] p
= this.p
;
206 double[] p_width
= this.p_width
;
208 if (!this.at
.isIdentity()) {
209 final Object
[] ob
= getTransformedData();
210 p
= (double[][])ob
[0];
211 p_width
= (double[])ob
[1];
214 final boolean no_color_cues
= "true".equals(project
.getProperty("no_color_cues"));
217 final int i_current
= layer_set
.getLayerIndex(active_layer
.getId());
220 for (int j
=0; j
<n_points
; j
++) {
221 ii
= layer_set
.getLayerIndex(p_layer
[j
]);
222 if (ii
== i_current
-1 && !no_color_cues
) g
.setColor(Color
.red
);
223 else if (ii
== i_current
) g
.setColor(this.color
);
224 else if (ii
== i_current
+ 1 && !no_color_cues
) g
.setColor(Color
.blue
);
225 else continue; //don't paint!
226 radius
= (int)p_width
[j
];
227 g
.drawOval((int)(p
[0][j
]) - radius
, (int)(p
[1][j
]) - radius
, radius
+ radius
, radius
+ radius
);
230 final long layer_id
= active_layer
.getId();
231 for (int j
=0; j
<n_points
; j
++) {
232 if (layer_id
!= p_layer
[j
]) continue;
233 DisplayCanvas
.drawHandle(g
, (int)p
[0][j
], (int)p
[1][j
], magnification
);
237 //Transparency: fix alpha composite back to original.
238 if (null != original_composite
) {
239 g
.setComposite(original_composite
);
243 public void keyPressed(KeyEvent ke
) {
247 /**Helper vars for mouse events. Safe as static since only one Ball will be edited at a time.*/
248 static int index
= -1;
250 public void mousePressed(MouseEvent me
, int x_p
, int y_p
, double mag
) {
251 // transform the x_p, y_p to the local coordinates
252 if (!this.at
.isIdentity()) {
253 final Point2D
.Double po
= inverseTransformPoint(x_p
, y_p
);
258 final int tool
= ProjectToolbar
.getToolId();
260 if (ProjectToolbar
.PEN
== tool
) {
261 long layer_id
= Display
.getFrontLayer().getId();
262 if (Utils
.isControlDown(me
) && me
.isShiftDown()) {
263 index
= findNearestPoint(p
, n_points
, x_p
, y_p
); // should go to an AbstractProfile or something
265 index
= findPoint(p
, x_p
, y_p
, mag
);
268 if (layer_id
== p_layer
[index
]) {
269 if (Utils
.isControlDown(me
) && me
.isShiftDown() && p_layer
[index
] == Display
.getFrontLayer().getId()) {
271 index
= -1; // to prevent saving in the database twice
275 } else index
= -1; // disable if not in the front layer (so a new point will be added)
277 // Make the radius for newly added balls that of the last selected
278 last_radius
= p_width
[index
];
282 index
= addPoint(x_p
, y_p
, mag
, layer_id
);
288 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
) {
289 // transform to the local coordinates
290 if (!this.at
.isIdentity()) {
291 final Point2D
.Double p
= inverseTransformPoint(x_p
, y_p
);
294 final Point2D
.Double pd
= inverseTransformPoint(x_d
, y_d
);
297 final Point2D
.Double pdo
= inverseTransformPoint(x_d_old
, y_d_old
);
298 x_d_old
= (int)pdo
.x
;
299 y_d_old
= (int)pdo
.y
;
302 final int tool
= ProjectToolbar
.getToolId();
304 if (ProjectToolbar
.PEN
== tool
) {
306 if (me
.isShiftDown()) {
307 p_width
[index
] = Math
.sqrt((x_d
- p
[0][index
])*(x_d
- p
[0][index
]) + (y_d
- p
[1][index
])*(y_d
- p
[1][index
]));
308 last_radius
= p_width
[index
];
309 Utils
.showStatus("radius: " + p_width
[index
], false);
311 dragPoint(index
, x_d
- x_d_old
, y_d
- y_d_old
);
318 public void mouseReleased(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_r
, int y_r
) {
320 //update points in database if there was any change
321 if (-1 != index
&& index
!= n_points
) { // don't do it when the last point is removed
322 // NEEDS to be able to identify each point separately!! Needs an id, or an index as in pipe!! //updateInDatabase(getUpdatePointForSQL(index));
323 updateInDatabase("points"); // delete and add all again. TEMPORARY
326 //later!//calculateBoundingBox(true);
327 updateInDatabase("transform+dimensions");
335 private void calculateBoundingBox(boolean adjust_position
) {
336 double min_x
= Double
.MAX_VALUE
;
337 double min_y
= Double
.MAX_VALUE
;
341 this.width
= this.height
= 0;
342 layer_set
.updateBucket(this);
346 for (int i
=0; i
<n_points
; i
++) {
347 if (p
[0][i
] - p_width
[i
] < min_x
) min_x
= p
[0][i
] - p_width
[i
];
348 if (p
[1][i
] - p_width
[i
] < min_y
) min_y
= p
[1][i
] - p_width
[i
];
349 if (p
[0][i
] + p_width
[i
] > max_x
) max_x
= p
[0][i
] + p_width
[i
];
350 if (p
[1][i
] + p_width
[i
] > max_y
) max_y
= p
[1][i
] + p_width
[i
];
353 this.width
= max_x
- min_x
;
354 this.height
= max_y
- min_y
;
356 if (adjust_position
) {
357 // now readjust points to make min_x,min_y be the x,y
358 for (int i
=0; i
<n_points
; i
++) {
359 p
[0][i
] -= min_x
; p
[1][i
] -= min_y
;
361 this.at
.translate(min_x
, min_y
); // not using super.translate(...) because a preConcatenation is not needed; here we deal with the data.
362 updateInDatabase("transform+dimensions");
364 updateInDatabase("dimensions");
367 layer_set
.updateBucket(this);
370 /**Release all memory resources taken by this object.*/
371 public void destroy() {
379 public void repaint() {
383 /**Repaints in the given ImageCanvas only the area corresponding to the bounding box of this Profile. */
384 public void repaint(boolean repaint_navigator
) {
385 //TODO: this could be further optimized to repaint the bounding box of the last modified segments, i.e. the previous and next set of interpolated points of any given backbone point. This would be trivial if each segment of the Bezier curve was an object.
386 Rectangle box
= getBoundingBox(null);
387 calculateBoundingBox(true);
388 box
.add(getBoundingBox(null));
389 Display
.repaint(layer_set
, this, box
, 5, repaint_navigator
);
392 /**Make this object ready to be painted.*/
393 private void setupForDisplay() {
396 ArrayList al
= project
.getLoader().fetchBallPoints(id
);
397 n_points
= al
.size();
398 p
= new double[2][n_points
];
399 p_layer
= new long[n_points
];
400 p_width
= new double[n_points
];
401 Iterator it
= al
.iterator();
403 while (it
.hasNext()) {
404 Object
[] ob
= (Object
[])it
.next();
405 p
[0][i
] = ((Double
)ob
[0]).doubleValue();
406 p
[1][i
] = ((Double
)ob
[1]).doubleValue();
407 p_width
[i
] = ((Double
)ob
[2]).doubleValue();
408 p_layer
[i
] = ((Long
)ob
[3]).longValue();
413 /**Release memory resources used by this object: namely the arrays of points, which can be reloaded with a call to setupForDisplay()*/
414 public void flush() {
418 n_points
= -1; // flag that points exist
421 /** The exact perimeter of this Ball, in integer precision. */
422 public Polygon
getPerimeter() {
423 if (-1 == n_points
) setupForDisplay();
425 // local pointers, since they may be transformed
426 double[][] p
= this.p
;
427 double[] p_width
= this.p_width
;
429 if (!this.at
.isIdentity()) {
430 final Object
[] ob
= getTransformedData();
431 p
= (double[][])ob
[0];
432 p_width
= (double[])ob
[1];
435 // the box of the selected point
436 return new Polygon(new int[]{(int)(p
[0][index
] - p_width
[index
]), (int)(p
[0][index
] + p_width
[index
]), (int)(p
[0][index
] + p_width
[index
]), (int)(p
[0][index
] - p_width
[index
])}, new int[]{(int)(p
[1][index
] - p_width
[index
]), (int)(p
[1][index
] + p_width
[index
]), (int)(p
[1][index
] + p_width
[index
]), (int)(p
[1][index
] - p_width
[index
])}, 4);
439 return super.getPerimeter();
442 /** Writes the data of this object as a Ball object in the .shapes file represented by the 'data' StringBuffer. */
443 public void toShapesFile(StringBuffer data
, String group
, String color
, double z_scale
) {
444 if (-1 == n_points
) setupForDisplay();
445 // TEMPORARY FIX: sort balls by layer_id (by Z, which is roughly the same)
446 final HashMap ht
= new HashMap();
448 // local pointers, since they may be transformed
449 double[][] p
= this.p
;
450 double[] p_width
= this.p_width
;
451 if (!this.at
.isIdentity()) {
452 final Object
[] ob
= getTransformedData();
453 p
= (double[][])ob
[0];
454 p_width
= (double[])ob
[1];
456 StringBuffer sb
= new StringBuffer();
457 sb
.append("type=ball").append(l
)
458 .append("name=").append(project
.getMeaningfulTitle(this)).append(l
)
459 .append("group=").append(group
).append(l
)
460 .append("color=").append(color
).append(l
)
461 .append("supergroup=").append("null").append(l
)
462 .append("supercolor=").append("null").append(l
)
465 StringBuffer tmp
= null;
466 for (int i
=0; i
<n_points
; i
++) {
467 Long layer_id
= new Long(p_layer
[i
]);
468 // Doesn't work ??//if (ht.contains(layer_id)) tmp = (StringBuffer)ht.get(layer_id);
469 for (Iterator it
= ht
.entrySet().iterator(); it
.hasNext(); ) {
470 Map
.Entry entry
= (Map
.Entry
)it
.next();
471 Long lid
= (Long
)entry
.getKey();
472 if (lid
.longValue() == p_layer
[i
]) {
473 tmp
= (StringBuffer
)entry
.getValue();
478 tmp
= new StringBuffer(sb
.toString()); // can't clone ?!?
479 tmp
.append(layer
.getParent().getLayer(p_layer
[i
]).getZ() * z_scale
).append(l
);
480 ht
.put(layer_id
, tmp
);
482 tmp
.append("x").append(p
[0][i
]).append(l
)
483 .append("y").append(p
[1][i
]).append(l
)
484 .append("r").append(p_width
[i
]).append(l
)
488 for (Iterator it
= ht
.values().iterator(); it
.hasNext(); ) {
489 tmp
= (StringBuffer
)it
.next();
490 data
.append(tmp
).append(l
);
492 Utils
.log("tmp : " + tmp
.toString());
496 /** Return the list of query statements needed to insert all the points in the database. */
497 public String
[] getPointsForSQL() {
498 String
[] sql
= new String
[n_points
];
499 for (int i
=0; i
<n_points
; i
++) {
500 StringBuffer sb
= new StringBuffer("INSERT INTO ab_ball_points (ball_id, x, y, width, layer_id) VALUES (");
501 sb
.append(this.id
).append(",")
502 .append(p
[0][i
]).append(",")
503 .append(p
[1][i
]).append(",")
504 .append(p_width
[i
]).append(",")
508 sql
[i
] = sb
.toString();
513 private String
getUpdatePointForSQL(int index
) {
514 if (index
< 0 || index
> n_points
-1) return null;
516 StringBuffer sb
= new StringBuffer("UPDATE ab_ball_points SET ");
517 sb
.append("x=").append(p
[0][index
])
518 .append(", y=").append(p
[1][index
])
519 .append(", width=").append(p_width
[index
])
520 .append(", layer_id=").append(p_layer
[index
])
521 .append(" WHERE ball_id=").append(this.id
)
523 return sb
.toString();
526 public boolean isDeletable() {
527 return 0 == n_points
;
530 /** Test whether the Ball contains the given point at the given layer. What it does: and tests whether the point is contained in any of the balls present in the given layer. */
531 public boolean contains(Layer layer
, int x
, int y
) {
532 if (-1 == n_points
) setupForDisplay(); // reload points
533 if (0 == n_points
) return false;
535 final Point2D
.Double po
= inverseTransformPoint(x
, y
);
539 final long layer_id
= layer
.getId();
540 for (int i
=0; i
<n_points
; i
++) {
541 if (layer_id
!= p_layer
[i
]) continue;
542 if (x
>= p
[0][i
] - p_width
[i
] && x
<= p
[0][i
] + p_width
[i
] && y
>= p
[1][i
] - p_width
[i
] && y
<= p
[1][i
] + p_width
[i
]) return true;
547 /** Get the perimeter of all parts that show in the given layer (as defined by its Z), but representing each ball as a square in a Rectangle object. Returns null if none found. */
548 private Rectangle
[] getSubPerimeters(final Layer layer
) {
549 final ArrayList al
= new ArrayList();
550 final long layer_id
= layer
.getId();
551 double[][] p
= this.p
;
552 double[] p_width
= this.p_width
;
553 if (!this.at
.isIdentity()) {
554 final Object
[] ob
= getTransformedData();
555 p
= (double[][])ob
[0];
556 p_width
= (double[])ob
[1];
558 for (int i
=0; i
<n_points
; i
++) {
559 if (layer_id
!= p_layer
[i
]) continue;
560 al
.add(new Rectangle((int)(p
[0][i
] - p_width
[i
]), (int)(p
[1][i
] - p_width
[i
]), (int)Math
.ceil(p_width
[i
] + p_width
[i
]), (int)Math
.ceil(p_width
[i
] + p_width
[i
]))); // transformRectangle returns a copy of the Rectangle
562 if (al
.isEmpty()) return null;
564 final Rectangle
[] rects
= new Rectangle
[al
.size()];
570 public void linkPatches() {
571 // 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:
572 unlinkAll(Patch
.class);
574 // scan the Display and link Patch objects that lay under this Profile's bounding box:
576 // catch all displayables of the current Layer
577 final ArrayList al
= layer
.getDisplayables(Patch
.class);
579 // this bounding box as in the present layer
580 final Rectangle
[] perimeters
= getSubPerimeters(layer
); // transformed
581 if (null == perimeters
) return;
583 // for each Patch, check if it underlays this profile's bounding box
584 final Rectangle box
= new Rectangle(); // as tmp
585 for (Iterator itd
= al
.iterator(); itd
.hasNext(); ) {
586 final Displayable displ
= (Displayable
)itd
.next();
587 // 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
588 for (int i
=0; i
<perimeters
.length
; i
++) {
589 if (perimeters
[i
].intersects(displ
.getBoundingBox(box
))) {
597 /** Returns the layer of lowest Z coordinate where this ZDisplayable has a point in, or the creation layer if no points yet. */
598 public Layer
getFirstLayer() {
599 if (0 == n_points
) return this.layer
;
600 if (-1 == n_points
) setupForDisplay(); //reload
601 Layer la
= this.layer
;
602 double z
= Double
.MAX_VALUE
;
603 for (int i
=0; i
<n_points
; i
++) {
604 Layer layer
= layer_set
.getLayer(p_layer
[i
]);
605 if (layer
.getZ() < z
) la
= layer
;
610 /** Returns a [n_points][4] array, with x,y,z,radius on the second part; not transformed, but local!
611 * To obtain balls in world coordinates, calibrated, use getWorldBalls().
613 public double[][] getBalls() {
614 if (-1 == n_points
) setupForDisplay(); // reload
615 final double[][] b
= new double[n_points
][4];
616 for (int i
=0; i
<n_points
; i
++) {
619 b
[i
][2] = layer_set
.getLayer(p_layer
[i
]).getZ();
620 b
[i
][3] = p_width
[i
];
625 /** Returns a [n_points][4] array, with x,y,z,radius on the second part, in world coordinates (that is, transformed with this AffineTransform and calibrated with the containing LayerSet's calibration). */
626 public double[][] getWorldBalls() {
627 if (-1 == n_points
) setupForDisplay(); // reload
628 final double[][] b
= new double[n_points
][4];
629 final Calibration cal
= getLayerSet().getCalibrationCopy();
630 final int sign
= cal
.pixelDepth
< 0 ?
-1 : 1;
631 for (int i
=0; i
<n_points
; i
++) {
632 final Point2D
.Double po
= transformPoint(p
[0][i
], p
[1][i
]); // bring to world coordinates
633 b
[i
][0] = po
.x
* cal
.pixelWidth
;
634 b
[i
][1] = po
.y
* cal
.pixelHeight
;
635 b
[i
][2] = layer_set
.getLayer(p_layer
[i
]).getZ() * cal
.pixelWidth
* sign
;
636 b
[i
][3] = p_width
[i
] * cal
.pixelWidth
;
641 public void exportSVG(StringBuffer data
, double z_scale
, String indent
) {
642 if (-1 == n_points
) setupForDisplay(); // reload
643 if (0 == n_points
) return;
644 String in
= indent
+ "\t";
645 String
[] RGB
= Utils
.getHexRGBColor(color
);
646 final double[] a
= new double[6];
648 data
.append(indent
).append("<ball_ob\n>")
649 .append(in
).append("id=\"").append(id
).append("\"")
650 .append(in
).append("transform=\"matrix(").append(a
[0]).append(',')
651 .append(a
[1]).append(',')
652 .append(a
[2]).append(',')
653 .append(a
[3]).append(',')
654 .append(a
[4]).append(',')
655 .append(a
[5]).append(")\"\n")
656 .append(in
).append("style=\"fill:none;stroke-opacity:").append(alpha
).append(";stroke:#").append(RGB
[0]).append(RGB
[1]).append(RGB
[2]).append(";stroke-width:1.0px;stroke-opacity:1.0\"\n")
657 .append(in
).append("links=\"")
659 if (null != hs_linked
&& 0 != hs_linked
.size()) {
661 int len
= hs_linked
.size();
662 for (Iterator it
= hs_linked
.iterator(); it
.hasNext(); ) {
663 Object ob
= it
.next();
664 data
.append(((DBObject
)ob
).getId());
665 if (ii
!= len
-1) data
.append(",");
670 .append(indent
).append(">\n");
671 for (int i
=0; i
<n_points
; i
++) {
672 data
.append(in
).append("<ball x=\"").append(p
[0][i
]).append("\" y=\"").append(p
[1][0]).append("\" z=\"").append(layer_set
.getLayer(p_layer
[i
]).getZ() * z_scale
).append("\" r=\"").append(p_width
[i
]).append("\" />\n");
674 data
.append(indent
).append("</ball_ob>\n");
677 /** Similar to exportSVG but the layer_id is saved instead of the z. The convention is my own, a ball_ob that contains ball objects and links. */
678 public void exportXML(StringBuffer sb_body
, String indent
, Object any
) {
679 if (-1 == n_points
) setupForDisplay(); // reload
680 //if (0 == n_points) return;
681 String in
= indent
+ "\t";
682 String
[] RGB
= Utils
.getHexRGBColor(color
);
683 sb_body
.append(indent
).append("<t2_ball\n");
684 super.exportXML(sb_body
, in
, any
);
685 sb_body
.append(in
).append("style=\"fill:none;stroke-opacity:").append(alpha
).append(";stroke:#").append(RGB
[0]).append(RGB
[1]).append(RGB
[2]).append(";stroke-width:1.0px;\"\n")
687 sb_body
.append(indent
).append(">\n");
688 for (int i
=0; i
<n_points
; i
++) {
689 sb_body
.append(in
).append("<t2_ball_ob x=\"").append(p
[0][i
]).append("\" y=\"").append(p
[1][i
]).append("\" layer_id=\"").append(p_layer
[i
]).append("\" r=\"").append(p_width
[i
]).append("\" />\n");
691 super.restXML(sb_body
, in
, any
);
692 sb_body
.append(indent
).append("</t2_ball>\n");
695 static public void exportDTD(StringBuffer sb_header
, HashSet hs
, String indent
) {
696 String type
= "t2_ball";
697 if (hs
.contains(type
)) return;
699 sb_header
.append(indent
).append("<!ELEMENT t2_ball (").append(Displayable
.commonDTDChildren()).append(",t2_ball_ob)>\n");
700 Displayable
.exportDTD(type
, sb_header
, hs
, indent
);
701 sb_header
.append(indent
).append("<!ELEMENT t2_ball_ob EMPTY>\n")
702 .append(indent
).append("<!ATTLIST t2_ball_ob x NMTOKEN #REQUIRED>\n")
703 .append(indent
).append("<!ATTLIST t2_ball_ob y NMTOKEN #REQUIRED>\n")
704 .append(indent
).append("<!ATTLIST t2_ball_ob r NMTOKEN #REQUIRED>\n")
705 .append(indent
).append("<!ATTLIST t2_ball_ob layer_id NMTOKEN #REQUIRED>\n")
709 /** */ // this may be inaccurate
710 public boolean paintsAt(Layer layer
) {
711 if (!super.paintsAt(layer
)) return false;
712 // find previous and next
713 final long lid_previous
= layer_set
.previous(layer
).getId(); // never null, may be the same though
714 final long lid_next
= layer_set
.next(layer
).getId(); // idem
715 final long lid
= layer
.getId();
716 for (int i
=0; i
<p_layer
.length
; i
++) {
717 if (lid
== p_layer
[i
] || lid_previous
== p_layer
[i
] || lid_next
== p_layer
[i
]) return true;
722 /** Returns information on the number of ball objects per layer. */
723 public String
getInfo() {
724 // group balls by layer
725 HashMap ht
= new HashMap();
726 for (int i
=0; i
<n_points
; i
++) {
727 ArrayList al
= (ArrayList
)ht
.get(new Long(p_layer
[i
]));
729 al
= new ArrayList();
730 ht
.put(new Long(p_layer
[i
]), al
);
732 al
.add(new Integer(i
)); // blankets!
735 StringBuffer sb1
= new StringBuffer("Ball id: ").append(this.id
).append('\n');
736 StringBuffer sb
= new StringBuffer();
737 for (Iterator it
= ht
.entrySet().iterator(); it
.hasNext(); ) {
738 Map
.Entry entry
= (Map
.Entry
)it
.next();
739 long lid
= ((Long
)entry
.getKey()).longValue();
740 ArrayList al
= (ArrayList
)entry
.getValue();
741 sb
.append("\tLayer ").append(this.layer_set
.getLayer(lid
).toString()).append(":\n");
742 sb
.append("\t\tcount : ").append(al
.size()).append('\n');
745 for (Iterator at
= al
.iterator(); at
.hasNext(); ) {
746 int i
= ((Integer
)at
.next()).intValue(); // I hate java
747 average
+= p_width
[i
];
749 sb
.append("\t\taverage radius: ").append(average
/ al
.size()).append('\n');
751 return sb1
.append("Total count: ").append(total
).append('\n').append(sb
).toString();
754 /** Performs a deep copy of this object, without the links. */
755 public Displayable
clone(final Project pr
, final boolean copy_id
) {
756 final long nid
= copy_id ?
this.id
: pr
.getLoader().getNextId();
757 final Ball copy
= new Ball(pr
, nid
, null != title ? title
.toString() : null, width
, height
, alpha
, this.visible
, new Color(color
.getRed(), color
.getGreen(), color
.getBlue()), this.locked
, (AffineTransform
)this.at
.clone());
758 // links are left null
760 if (-1 == n_points
) setupForDisplay(); // load data
761 copy
.n_points
= n_points
;
762 copy
.p
= new double[][]{(double[])this.p
[0].clone(), (double[])this.p
[1].clone()};
763 copy
.p_layer
= (long[])this.p_layer
.clone();
764 copy
.p_width
= (double[])this.p_width
.clone();
765 copy
.addToDatabase();
770 /** Generate a globe of radius 1.0 that can be used for any Ball. First dimension is Z, then comes a double array x,y. Minimal accepted meridians and parallels is 3.*/
771 static public double[][][] generateGlobe(int meridians
, int parallels
) {
772 if (meridians
< 3) meridians
= 3;
773 if (parallels
< 3) parallels
= 3;
775 -first loop makes horizontal circle using meridian points.
776 -second loop scales it appropiately and makes parallels.
777 Both loops are common for all balls and so should be done just once.
778 Then this globe can be properly translocated and resized for each ball.
780 // a circle of radius 1
781 double angle_increase
= 2*Math
.PI
/ meridians
;
782 double temp_angle
= 0;
783 final double[][] xy_points
= new double[meridians
+1][2]; //plus 1 to repeat last point
784 xy_points
[0][0] = 1; // first point
786 for (int m
=1; m
<meridians
; m
++) {
787 temp_angle
= angle_increase
*m
;
788 xy_points
[m
][0] = Math
.cos(temp_angle
);
789 xy_points
[m
][1] = Math
.sin(temp_angle
);
791 xy_points
[xy_points
.length
-1][0] = 1; // last point
792 xy_points
[xy_points
.length
-1][1] = 0;
794 // Build parallels from circle
795 angle_increase
= Math
.PI
/ parallels
; // = 180 / parallels in radians
796 final double angle90
= Math
.toRadians(90);
797 final double[][][] xyz
= new double[parallels
+1][xy_points
.length
][3];
798 for (int p
=1; p
<xyz
.length
-1; p
++) {
799 double radius
= Math
.sin(angle_increase
*p
);
800 double Z
= Math
.cos(angle_increase
*p
);
801 for (int mm
=0; mm
<xyz
[0].length
-1; mm
++) {
802 //scaling circle to apropiate radius, and positioning the Z
803 xyz
[p
][mm
][0] = xy_points
[mm
][0] * radius
;
804 xyz
[p
][mm
][1] = xy_points
[mm
][1] * radius
;
807 xyz
[p
][xyz
[0].length
-1][0] = xyz
[p
][0][0]; //last one equals first one
808 xyz
[p
][xyz
[0].length
-1][1] = xyz
[p
][0][1];
809 xyz
[p
][xyz
[0].length
-1][2] = xyz
[p
][0][2];
812 // south and north poles
813 for (int ns
=0; ns
<xyz
[0].length
; ns
++) {
814 xyz
[0][ns
][0] = 0; //south pole
817 xyz
[xyz
.length
-1][ns
][0] = 0; //north pole
818 xyz
[xyz
.length
-1][ns
][1] = 0;
819 xyz
[xyz
.length
-1][ns
][2] = -1;
826 /** Put all balls as a single 'mesh'; the returned list contains all faces as three consecutive Point3f. The mesh is also translated by x,y,z of this Displayable.*/
827 public List
generateTriangles(final double scale
, final double[][][] globe
) {
829 Class c
= Class
.forName("javax.vecmath.Point3f");
830 } catch (ClassNotFoundException cnfe
) {
831 Utils
.log("Java3D is not installed.");
834 final Calibration cal
= layer_set
.getCalibrationCopy();
835 // modify the globe to fit each ball's radius and x,y,z position
836 final ArrayList list
= new ArrayList();
838 // local pointers, since they may be transformed
839 double[][] p
= this.p
;
840 double[] p_width
= this.p_width
;
841 if (!this.at
.isIdentity()) {
842 final Object
[] ob
= getTransformedData();
843 p
= (double[][])ob
[0];
844 p_width
= (double[])ob
[1];
846 final int sign
= cal
.pixelDepth
< 0 ?
-1 : 1;
848 for (int i
=0; i
<n_points
; i
++) {
849 // create local globe for the ball, and translate it to z,y,z
850 final double[][][] ball
= new double[globe
.length
][globe
[0].length
][3];
851 for (int z
=0; z
<ball
.length
; z
++) {
852 for (int k
=0; k
<ball
[0].length
; k
++) {
853 // the line below says: to each globe point, multiply it by the radius of the particular ball, then translate to the ball location, then translate to this Displayable's location, then scale to the Display3D scale.
854 ball
[z
][k
][0] = (globe
[z
][k
][0] * p_width
[i
] + p
[0][i
]) * scale
* cal
.pixelWidth
;
855 ball
[z
][k
][1] = (globe
[z
][k
][1] * p_width
[i
] + p
[1][i
]) * scale
* cal
.pixelHeight
;
856 ball
[z
][k
][2] = (globe
[z
][k
][2] * p_width
[i
] + layer_set
.getLayer(p_layer
[i
]).getZ()) * scale
* cal
.pixelWidth
* sign
; // not pixelDepth, see day notes 20080227. Because pixelDepth is in microns/px, not in px/microns, and the z coord here is taken from the z of the layer, which is in pixels.
859 // create triangular faces and add them to the list
860 for (int z
=0; z
<ball
.length
-1; z
++) { // the parallels
861 for (int k
=0; k
<ball
[0].length
-1; k
++) { // meridian points
862 // half quadrant (a triangle)
863 list
.add(new Point3f((float)ball
[z
][k
][0], (float)ball
[z
][k
][1], (float)ball
[z
][k
][2]));
864 list
.add(new Point3f((float)ball
[z
+1][k
+1][0], (float)ball
[z
+1][k
+1][1], (float)ball
[z
+1][k
+1][2]));
865 list
.add(new Point3f((float)ball
[z
+1][k
][0], (float)ball
[z
+1][k
][1], (float)ball
[z
+1][k
][2]));
866 // the other half quadrant
867 list
.add(new Point3f((float)ball
[z
][k
][0], (float)ball
[z
][k
][1], (float)ball
[z
][k
][2]));
868 list
.add(new Point3f((float)ball
[z
][k
+1][0], (float)ball
[z
][k
+1][1], (float)ball
[z
][k
+1][2]));
869 list
.add(new Point3f((float)ball
[z
+1][k
+1][0], (float)ball
[z
+1][k
+1][1], (float)ball
[z
+1][k
+1][2]));
871 // the Point3f could be initialized through reflection, by getting the Construntor from the Class and calling new Instance(new Object[]{new Double(x), new Double(y), new Double(z)), so it would compile even in the absence of java3d
877 /** Apply the AffineTransform to a copy of the points and return the arrays. */
878 private final Object
[] getTransformedData() {
880 final double[][] p
= transformPoints(this.p
);
881 // create points to represent the point where the radius ends. Since these are abstract spheres, there's no need to consider a second point that would provide the shear. To capture both the X and Y axis deformations, I use a diagonal point which sits at (x,y) => (p[0][i] + p_width[i], p[1][i] + p_width[i])
882 double[][] pw
= new double[2][n_points
];
883 for (int i
=0; i
<n_points
; i
++) {
884 pw
[0][i
] = this.p
[0][i
] + p_width
[i
]; //built relative to the untransformed points!
885 pw
[1][i
] = this.p
[1][i
] + p_width
[i
];
887 pw
= transformPoints(pw
);
888 final double[] p_width
= new double[n_points
];
889 for (int i
=0; i
<n_points
; i
++) {
890 // plain average of differences in X and Y axis, relative to the transformed points.
891 p_width
[i
] = (Math
.abs(pw
[0][i
] - p
[0][i
]) + Math
.abs(pw
[1][i
] - p
[1][i
])) / 2;
893 return new Object
[]{p
, p_width
};
896 /** @param area is expected in world coordinates. */
897 public boolean intersects(final Area area
, final double z_first
, final double z_last
) {
898 // find lowest and highest Z
899 double min_z
= Double
.MAX_VALUE
;
901 for (int i
=0; i
<n_points
; i
++) {
902 double laz
=layer_set
.getLayer(p_layer
[i
]).getZ();
903 if (laz
< min_z
) min_z
= laz
;
904 if (laz
> max_z
) max_z
= laz
;
906 if (z_last
< min_z
|| z_first
> max_z
) return false;
908 for (int i
=0; i
<n_points
; i
++) {
909 final Rectangle
[] rec
= getSubPerimeters(layer_set
.getLayer(p_layer
[i
]));
910 for (int k
=0; k
<rec
.length
; k
++) {
911 Area a
= new Area(rec
[k
]).createTransformedArea(this.at
);
913 Rectangle r
= a
.getBounds();
914 if (0 != r
.width
&& 0 != r
.height
) return true;
920 /** Returns a listing of all balls contained here, one per row with index, x, y, z, and radius, all calibrated.
921 * 'name-id' is a column that displays the title of this Ball object only when such title is purely a number.
924 public ResultsTable
measure(ResultsTable rt
) {
925 if (-1 == n_points
) setupForDisplay(); //reload
926 if (0 == n_points
) return rt
;
927 if (null == rt
) rt
= Utils
.createResultsTable("Ball results", new String
[]{"id", "index", "x", "y", "z", "radius", "name-id"});
928 final Object
[] ob
= getTransformedData();
929 double[][] p
= (double[][])ob
[0];
930 double[] p_width
= (double[])ob
[1];
931 final Calibration cal
= layer_set
.getCalibration();
932 for (int i
=0; i
<n_points
; i
++) {
933 rt
.incrementCounter();
934 rt
.addLabel("units", cal
.getUnit());
935 rt
.addValue(0, this.id
);
937 rt
.addValue(2, p
[0][i
] * cal
.pixelWidth
);
938 rt
.addValue(3, p
[1][i
] * cal
.pixelHeight
);
939 rt
.addValue(4, layer_set
.getLayer(p_layer
[i
]).getZ() * cal
.pixelWidth
);
940 rt
.addValue(5, p_width
[i
] * cal
.pixelWidth
);
941 rt
.addValue(6, getNameId());
947 Class
getInternalDataPackageClass() {
952 Object
getDataPackage() {
953 return new DPBall(this);
956 static private final class DPBall
extends Displayable
.DataPackage
{
958 final double[] p_width
;
959 final long[] p_layer
;
961 DPBall(final Ball ball
) {
963 // store copies of all arrays
964 this.p
= new double[][]{Utils
.copy(ball
.p
[0], ball
.n_points
), Utils
.copy(ball
.p
[1], ball
.n_points
)};
965 this.p_width
= Utils
.copy(ball
.p_width
, ball
.n_points
);
966 this.p_layer
= new long[ball
.n_points
]; System
.arraycopy(ball
.p_layer
, 0, this.p_layer
, 0, ball
.n_points
);
968 final boolean to2(final Displayable d
) {
970 final Ball ball
= (Ball
)d
;
971 final int len
= p
[0].length
; // == n_points, since it was cropped on copy
972 ball
.p
= new double[][]{Utils
.copy(p
[0], len
), Utils
.copy(p
[1], len
)};
973 ball
.n_points
= p
[0].length
;
974 ball
.p_layer
= new long[len
]; System
.arraycopy(p_layer
, 0, ball
.p_layer
, 0, len
);
975 ball
.p_width
= Utils
.copy(p_width
, len
);