preparing release pom-trakem2-2.0.0, VectorString-2.0.0, TrakEM2_-1.0h
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / Ball.java
blobc374a0ba1da0700831d89d886c1c44f3aae8e0be
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.display;
25 import java.awt.AlphaComposite;
26 import java.awt.Color;
27 import java.awt.Composite;
28 import java.awt.Graphics2D;
29 import java.awt.Polygon;
30 import java.awt.Rectangle;
31 import java.awt.Stroke;
32 import java.awt.event.KeyEvent;
33 import java.awt.event.MouseEvent;
34 import java.awt.geom.AffineTransform;
35 import java.awt.geom.Area;
36 import java.awt.geom.Ellipse2D;
37 import java.awt.geom.NoninvertibleTransformException;
38 import java.awt.geom.Point2D;
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.HashMap;
42 import java.util.HashSet;
43 import java.util.Iterator;
44 import java.util.List;
45 import java.util.Map;
47 import org.scijava.vecmath.Point3f;
49 import ij.gui.GenericDialog;
50 import ij.measure.Calibration;
51 import ij.measure.ResultsTable;
52 import ini.trakem2.Project;
53 import ini.trakem2.persistence.XMLOptions;
54 import ini.trakem2.utils.IJError;
55 import ini.trakem2.utils.M;
56 import ini.trakem2.utils.ProjectToolbar;
57 import ini.trakem2.utils.Utils;
59 public class Ball extends ZDisplayable implements VectorData {
61 /**The number of points.*/
62 protected int n_points;
63 /**The array of clicked points.*/
64 protected double[][] p;
65 /**The array of Layers over which each point lives */
66 protected long[] p_layer;
67 /**The width of each point. */
68 protected double[] p_width;
70 /** Every new Ball will have, for its first point, the last user-adjusted radius value or the radius of the last user-selected point. */
71 static private double last_radius = -1;
73 /** Paint as outlines (false) or as solid areas (true; default, with a default alpha of 0.4f).*/
74 private boolean fill_paint = true;
76 public Ball(final Project project, final String title, final double x, final double y) {
77 super(project, title, x, y);
78 n_points = 0;
79 p = new double[2][5];
80 p_layer = new long[5]; // the ids of the layers in which each point lays
81 p_width = new double[5];
82 addToDatabase();
85 /** Construct an unloaded Ball from the database. Points will be loaded later, when needed. */
86 public Ball(final Project project, final long id, final String title, final float width, final float height, final float alpha, final boolean visible, final Color color, final boolean locked, final AffineTransform at) {
87 super(project, id, title, locked, at, width, height);
88 this.visible = visible;
89 this.alpha = alpha;
90 this.color = color;
91 this.n_points = -1; //used as a flag to signal "I have points, but unloaded"
94 /** Construct a Ball from an XML entry. */
95 public Ball(final Project project, final long id, final HashMap<String,String> ht, final HashMap<Displayable,String> ht_links) {
96 super(project, id, ht, ht_links);
97 // individual balls will be added as soon as parsed
98 this.n_points = 0;
99 this.p = new double[2][5];
100 this.p_layer = new long[5];
101 this.p_width = new double[5];
103 final Object ob_data = ht.get("fill");
104 try {
105 if (null != ob_data) this.fill_paint = "true".equals(((String)ob_data).trim().toLowerCase()); // fails: //Boolean.getBoolean((String)ob_data);
106 } catch (final Exception e) {
107 Utils.log("Ball: could not read fill_paint value from XML:" + e);
111 /** Used to add individual ball objects when parsing. */
112 public void addBall(final double x, final double y, final double r, final long layer_id) {
113 if (p[0].length == n_points) enlargeArrays();
114 p[0][n_points] = x;
115 p[1][n_points] = y;
116 p_width[n_points] = r;
117 p_layer[n_points] = layer_id;
118 n_points++;
121 /**Increase the size of the arrays by 5.*/
122 private void enlargeArrays() {
123 //catch length
124 final int length = p[0].length;
125 //make copies
126 final double[][] p_copy = new double[2][length + 5];
127 final long[] p_layer_copy = new long[length + 5];
128 final double[] p_width_copy = new double[length + 5];
129 //copy values
130 System.arraycopy(p[0], 0, p_copy[0], 0, length);
131 System.arraycopy(p[1], 0, p_copy[1], 0, length);
132 System.arraycopy(p_layer, 0, p_layer_copy, 0, length);
133 System.arraycopy(p_width, 0, p_width_copy, 0, length);
134 //assign them
135 this.p = p_copy;
136 this.p_layer = p_layer_copy;
137 this.p_width = p_width_copy;
140 /**Find a point in an array, with a precision dependent on the magnification.*/
141 protected int findPoint(final double[][] a, final int x_p, final int y_p, final double magnification, final long lid) {
142 int index = -1;
143 double d = (10.0D / magnification);
144 if (d < 4) d = 4;
145 for (int i=0; i<n_points; i++) {
146 if (p_layer[i] != lid) continue;
147 if ((Math.abs(x_p - a[0][i]) + Math.abs(y_p - a[1][i])) <= p_width[i]) {
148 index = i;
151 return index;
153 /**Remove a point.*/
154 protected void removePoint(final int index) {
155 // check preconditions:
156 if (index < 0) {
157 return;
158 } else if (n_points - 1 == index) {
159 //last point out
160 n_points--;
161 } else {
162 //one point out (but not the last)
163 --n_points;
165 // shift all points after 'index' one position to the left:
166 for (int i=index; i<n_points; i++) {
167 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.
168 p[1][i] = p[1][i+1];
169 p_layer[i] = p_layer[i+1];
170 p_width[i] = p_width[i+1];
174 //later! Otherwise can't repaint properly//calculateBoundingBox(true);
176 //update in database
177 updateInDatabase("points");
180 /**Move backbone point by the given deltas.*/
181 private void dragPoint(final int index, final int dx, final int dy) {
182 p[0][index] += dx;
183 p[1][index] += dy;
186 static private double getFirstWidth() {
187 if (null == Display.getFront()) return 10;
188 if (-1 != last_radius) return last_radius;
189 return 10 / Display.getFront().getCanvas().getMagnification(); // 10 pixels in the screen
192 /**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.*/
193 protected int addPoint(final double x_p, final double y_p, final long layer_id, final double radius) {
194 if (-1 == n_points) setupForDisplay(); //reload
195 //check array size
196 if (p[0].length == n_points) {
197 enlargeArrays();
199 //append at the end
200 p[0][n_points] = x_p;
201 p[1][n_points] = y_p;
202 p_layer[n_points] = layer_id;
203 p_width[n_points] = radius;
204 index = n_points;
205 //add one up
206 this.n_points++;
207 updateInDatabase(new StringBuilder("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());
208 return index;
211 @Override
212 public void paint(final Graphics2D g, final Rectangle srcRect, final double magnification, final boolean active, final int channels, final Layer active_layer, final List<Layer> layers) {
213 if (0 == n_points) return;
214 if (-1 == n_points) {
215 // load points from the database
216 setupForDisplay();
218 //arrange transparency
219 final Composite original_composite = g.getComposite(),
220 perimeter_composite = alpha == 1.0f ? original_composite : AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha),
221 area_composite = fill_paint ? (alpha > 0.4f ? AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f) : perimeter_composite)
222 : null;
224 // Clear transform and stroke
225 final AffineTransform gt = g.getTransform();
226 g.setTransform(DisplayCanvas.DEFAULT_AFFINE);
227 final Stroke stroke = g.getStroke();
228 g.setStroke(DisplayCanvas.DEFAULT_STROKE);
230 // local pointers, since they may be transformed
231 double[][] p = this.p;
232 double[] p_width = this.p_width;
234 if (!this.at.isIdentity()) {
235 final Object[] ob = getTransformedData();
236 p = (double[][])ob[0];
237 p_width = (double[])ob[1];
240 final boolean color_cues = layer_set.color_cues;
241 final int n_layers_color_cue = layer_set.n_layers_color_cue;
242 final Color below, above;
243 if (layer_set.use_color_cue_colors) {
244 below = Color.red;
245 above = Color.blue;
246 } else {
247 below = this.color;
248 above = this.color;
251 // Paint a sliced sphere
252 final double current_layer_z = active_layer.getZ();
253 final int current_layer_index = layer_set.indexOf(active_layer);
254 final long active_lid = active_layer.getId();
255 for (int j=0; j<n_points; j++) {
256 if (active_lid == p_layer[j]) {
257 g.setColor(this.color);
258 final int radius = (int)p_width[j];
259 final int x = (int)((p[0][j] -radius -srcRect.x) * magnification),
260 y = (int)((p[1][j] -radius -srcRect.y) * magnification),
261 w = (int)(2 * radius * magnification);
262 if (fill_paint) {
263 g.setComposite(area_composite);
264 g.fillOval(x, y, w, w);
266 g.setComposite(perimeter_composite);
267 g.drawOval(x, y, w, w);
268 } else if (color_cues) {
269 boolean can_paint = -1 == n_layers_color_cue;
270 final Layer layer = layer_set.getLayer(p_layer[j]); // fast: map lookup
271 if (!can_paint) {
272 can_paint = Math.abs(current_layer_index - layer_set.indexOf(layer)) <= n_layers_color_cue; // fast: map lookup
274 // Check if p_layer[j] is within the range of layers to color cue:
275 //Utils.logMany2("current_layer_index: ", current_layer_index, "layer index:", layer_set.indexOf(layer), "n_layers_color_cue", n_layers_color_cue);
276 if (can_paint) {
277 // does the point intersect with the layer?
278 final double z = layer.getZ();
279 final double depth = Math.abs(current_layer_z - z);
280 if (depth < this.p_width[j]) { // compare with untransformed data, in pixels!
281 // intersects!
282 if (z < current_layer_z) g.setColor(below);
283 else g.setColor(above);
284 // h^2 = sin^2 + cos^2 ---> p_width[j] is h, and sin*h is depth
285 final int slice_radius = (int)(p_width[j] * Math.sqrt(1 - Math.pow(depth/p_width[j], 2)));
286 final int x = (int)((p[0][j] -slice_radius -srcRect.x) * magnification),
287 y = (int)((p[1][j] -slice_radius -srcRect.y) * magnification),
288 w = (int)(2 * slice_radius * magnification);
289 if (fill_paint) {
290 g.setComposite(area_composite);
291 g.fillOval(x, y, w, w);
293 g.setComposite(perimeter_composite);
294 g.drawOval(x, y, w, w);
300 if (active) {
301 final long layer_id = active_layer.getId();
302 for (int j=0; j<n_points; j++) {
303 if (layer_id != p_layer[j]) continue;
304 DisplayCanvas.drawScreenHandle(g, (int)((p[0][j] -srcRect.x) * magnification),
305 (int)((p[1][j] -srcRect.y) * magnification));
309 //Transparency: fix alpha composite back to original.
310 g.setComposite(original_composite);
312 // Restore
313 g.setTransform(gt);
314 g.setStroke(stroke);
317 @Override
318 public void keyPressed(final KeyEvent ke) {
319 // TODO
322 /**Helper vars for mouse events. Safe as static since only one Ball will be edited at a time.*/
323 static int index = -1;
325 @Override
326 public void mousePressed(final MouseEvent me, final Layer layer, int x_p, int y_p, final double mag) {
327 // transform the x_p, y_p to the local coordinates
328 if (!this.at.isIdentity()) {
329 final Point2D.Double po = inverseTransformPoint(x_p, y_p);
330 x_p = (int)po.x;
331 y_p = (int)po.y;
334 final int tool = ProjectToolbar.getToolId();
336 if (ProjectToolbar.PEN == tool) {
337 final long layer_id = layer.getId();
338 if (Utils.isControlDown(me) && me.isShiftDown()) {
339 index = findNearestPoint(p, p_layer, n_points, x_p, y_p, layer.getId()); // should go to an AbstractProfile or something
340 } else {
341 index = findPoint(p, x_p, y_p, mag, layer.getId());
343 if (-1 != index) {
344 if (layer_id == p_layer[index]) {
345 if (Utils.isControlDown(me) && me.isShiftDown() && p_layer[index] == Display.getFrontLayer().getId()) {
346 removePoint(index);
347 index = -1; // to prevent saving in the database twice
348 repaint(false, layer);
349 return;
351 } else index = -1; // disable if not in the front layer (so a new point will be added)
352 if (-1 != index) {
353 // Make the radius for newly added balls that of the last selected
354 last_radius = p_width[index];
357 if (-1 == index) {
358 index = addPoint(x_p, y_p, layer_id, (0 == n_points ? Ball.getFirstWidth() : p_width[n_points -1])); // either 10 screen pixels or the same as the last point
359 repaint(false, layer);
364 @Override
365 public void mouseDragged(final MouseEvent me, final Layer layer, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old) {
366 // transform to the local coordinates
367 if (!this.at.isIdentity()) {
368 final Point2D.Double p = inverseTransformPoint(x_p, y_p);
369 x_p = (int)p.x;
370 y_p = (int)p.y;
371 final Point2D.Double pd = inverseTransformPoint(x_d, y_d);
372 x_d = (int)pd.x;
373 y_d = (int)pd.y;
374 final Point2D.Double pdo = inverseTransformPoint(x_d_old, y_d_old);
375 x_d_old = (int)pdo.x;
376 y_d_old = (int)pdo.y;
379 final int tool = ProjectToolbar.getToolId();
381 if (ProjectToolbar.PEN == tool) {
382 if (-1 != index) {
383 if (me.isShiftDown()) {
384 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]));
385 last_radius = p_width[index];
386 Utils.showStatus("radius: " + p_width[index], false);
387 } else {
388 dragPoint(index, x_d - x_d_old, y_d - y_d_old);
390 repaint(false, layer);
395 @Override
396 public void mouseReleased(final MouseEvent me, final Layer layer, final int x_p, final int y_p, final int x_d, final int y_d, final int x_r, final int y_r) {
398 //update points in database if there was any change
399 if (-1 != index && index != n_points) { // don't do it when the last point is removed
400 // NEEDS to be able to identify each point separately!! Needs an id, or an index as in pipe!! //updateInDatabase(getUpdatePointForSQL(index));
401 updateInDatabase("points"); // delete and add all again. TEMPORARY
403 if (-1 != index) {
404 //later!//calculateBoundingBox(true);
405 updateInDatabase("transform+dimensions");
408 // reset
409 index = -1;
410 repaint(true, layer);
413 @Override
414 protected boolean calculateBoundingBox(final Layer la) {
415 calculateBoundingBox(true, la);
416 return true;
419 /** Uses the @param layer to update a specific Bucket for that layer. */
420 private void calculateBoundingBox(final boolean adjust_position, final Layer la) {
421 double min_x = Double.MAX_VALUE;
422 double min_y = Double.MAX_VALUE;
423 double max_x = 0.0D;
424 double max_y = 0.0D;
425 if (0 == n_points) {
426 this.width = this.height = 0;
427 updateBucket(la);
428 return;
430 if (0 != n_points) {
431 for (int i=0; i<n_points; i++) {
432 if (p[0][i] - p_width[i] < min_x) min_x = p[0][i] - p_width[i];
433 if (p[1][i] - p_width[i] < min_y) min_y = p[1][i] - p_width[i];
434 if (p[0][i] + p_width[i] > max_x) max_x = p[0][i] + p_width[i];
435 if (p[1][i] + p_width[i] > max_y) max_y = p[1][i] + p_width[i];
438 this.width = (float)(max_x - min_x);
439 this.height = (float)(max_y - min_y);
441 if (adjust_position) {
442 // now readjust points to make min_x,min_y be the x,y
443 for (int i=0; i<n_points; i++) {
444 p[0][i] -= min_x; p[1][i] -= min_y;
446 this.at.translate(min_x, min_y); // not using super.translate(...) because a preConcatenation is not needed; here we deal with the data.
447 updateInDatabase("transform+dimensions");
448 } else {
449 updateInDatabase("dimensions");
452 updateBucket(la);
455 /**Release all memory resources taken by this object.*/
456 @Override
457 public void destroy() {
458 super.destroy();
459 p = null;
460 p_layer = null;
461 p_width = null;
464 /**Repaints in the given ImageCanvas only the area corresponding to the bounding box of this Profile. */
465 public void repaint(final boolean repaint_navigator, final Layer layer) {
466 //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.
467 final Rectangle box = getBoundingBox(null);
468 calculateBoundingBox(true, layer);
469 box.add(getBoundingBox(null));
470 Display.repaint(layer_set, this, box, 5, repaint_navigator);
473 /**Make this object ready to be painted.*/
474 private void setupForDisplay() {
475 // load points
476 if (null == p) {
477 final ArrayList<?> al = project.getLoader().fetchBallPoints(id);
478 n_points = al.size();
479 p = new double[2][n_points];
480 p_layer = new long[n_points];
481 p_width = new double[n_points];
482 final Iterator<?> it = al.iterator();
483 int i = 0;
484 while (it.hasNext()) {
485 final Object[] ob = (Object[])it.next();
486 p[0][i] = ((Double)ob[0]).doubleValue();
487 p[1][i] = ((Double)ob[1]).doubleValue();
488 p_width[i] = ((Double)ob[2]).doubleValue();
489 p_layer[i] = ((Long)ob[3]).longValue();
490 i++;
494 /**Release memory resources used by this object: namely the arrays of points, which can be reloaded with a call to setupForDisplay()*/
495 public void flush() {
496 p = null;
497 p_width = null;
498 p_layer = null;
499 n_points = -1; // flag that points exist
502 /** The exact perimeter of this Ball, in integer precision. */
503 @Override
504 public Polygon getPerimeter() {
505 if (-1 == n_points) setupForDisplay();
507 // local pointers, since they may be transformed
508 double[][] p = this.p;
509 double[] p_width = this.p_width;
511 if (!this.at.isIdentity()) {
512 final Object[] ob = getTransformedData();
513 p = (double[][])ob[0];
514 p_width = (double[])ob[1];
516 if (-1 != index) {
517 // the box of the selected point
518 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);
519 } else {
520 // the whole box
521 return super.getPerimeter();
524 /** Writes the data of this object as a Ball object in the .shapes file represented by the 'data' StringBuffer. */
525 public void toShapesFile(final StringBuffer data, final String group, final String color, final double z_scale) {
526 if (-1 == n_points) setupForDisplay();
527 // TEMPORARY FIX: sort balls by layer_id (by Z, which is roughly the same)
528 final HashMap<Long,StringBuffer> ht = new HashMap<Long,StringBuffer>();
529 final char l = '\n';
530 // local pointers, since they may be transformed
531 double[][] p = this.p;
532 double[] p_width = this.p_width;
533 if (!this.at.isIdentity()) {
534 final Object[] ob = getTransformedData();
535 p = (double[][])ob[0];
536 p_width = (double[])ob[1];
538 final StringBuffer sb = new StringBuffer();
539 sb.append("type=ball").append(l)
540 .append("name=").append(project.getMeaningfulTitle(this)).append(l)
541 .append("group=").append(group).append(l)
542 .append("color=").append(color).append(l)
543 .append("supergroup=").append("null").append(l)
544 .append("supercolor=").append("null").append(l)
545 .append("in slice=")
547 StringBuffer tmp = null;
548 for (int i=0; i<n_points; i++) {
549 final Long layer_id = new Long(p_layer[i]);
550 // Doesn't work ??//if (ht.contains(layer_id)) tmp = (StringBuffer)ht.get(layer_id);
551 for (final Map.Entry<Long,StringBuffer> e : ht.entrySet()) {
552 if (e.getKey().longValue() == p_layer[i]) {
553 tmp = e.getValue();
556 if (null == tmp) {
557 //else {
558 tmp = new StringBuffer(sb.toString()); // can't clone ?!?
559 tmp.append(layer.getParent().getLayer(p_layer[i]).getZ() * z_scale).append(l);
560 ht.put(layer_id, tmp);
562 tmp.append("x").append(p[0][i]).append(l)
563 .append("y").append(p[1][i]).append(l)
564 .append("r").append(p_width[i]).append(l)
566 tmp = null;
568 for (final StringBuffer s : ht.values()) {
569 data.append(s).append(l);
571 Utils.log("s : " + s.toString());
575 /** Return the list of query statements needed to insert all the points in the database. */
576 public String[] getPointsForSQL() {
577 final String[] sql = new String[n_points];
578 for (int i=0; i<n_points; i++) {
579 final StringBuilder sb = new StringBuilder("INSERT INTO ab_ball_points (ball_id, x, y, width, layer_id) VALUES (");
580 sb.append(this.id).append(",")
581 .append(p[0][i]).append(",")
582 .append(p[1][i]).append(",")
583 .append(p_width[i]).append(",")
584 .append(p_layer[i])
585 .append(")");
586 ; //end
587 sql[i] = sb.toString();
589 return sql;
592 @Override
593 public boolean isDeletable() {
594 return 0 == n_points;
597 /** 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. */
598 @Override
599 public boolean contains(final Layer layer, double x, double y) {
600 if (-1 == n_points) setupForDisplay(); // reload points
601 if (0 == n_points) return false;
602 // make x,y local
603 final Point2D.Double po = inverseTransformPoint(x, y);
604 x = po.x;
605 y = po.y;
607 final long layer_id = layer.getId();
608 for (int i=0; i<n_points; i++) {
609 if (layer_id != p_layer[i]) continue;
610 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;
612 return false;
615 /** 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. */
616 private Rectangle[] getSubPerimeters(final Layer layer) {
617 final ArrayList<Rectangle> al = new ArrayList<Rectangle>();
618 final long layer_id = layer.getId();
619 double[][] p = this.p;
620 double[] p_width = this.p_width;
621 if (!this.at.isIdentity()) {
622 final Object[] ob = getTransformedData();
623 p = (double[][])ob[0];
624 p_width = (double[])ob[1];
626 for (int i=0; i<n_points; i++) {
627 if (layer_id != p_layer[i]) continue;
628 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
630 if (al.isEmpty()) return null;
631 else {
632 final Rectangle[] rects = new Rectangle[al.size()];
633 al.toArray(rects);
634 return rects;
638 @Override
639 public boolean linkPatches() {
640 // 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:
641 unlinkAll(Patch.class);
643 // scan the Display and link Patch objects that lay under this Profile's bounding box:
645 // catch all displayables of the current Layer
646 final ArrayList<Displayable> al = layer.getDisplayables(Patch.class);
648 // this bounding box as in the present layer
649 final Rectangle[] perimeters = getSubPerimeters(layer); // transformed
650 if (null == perimeters) return false;
652 boolean must_lock = false;
654 // for each Patch, check if it underlays this profile's bounding box
655 final Rectangle box = new Rectangle(); // as tmp
656 for (final Displayable displ : al) {
657 // 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
658 for (int i=0; i<perimeters.length; i++) {
659 if (perimeters[i].intersects(displ.getBoundingBox(box))) {
660 // Link the patch
661 this.link(displ);
662 if (displ.locked) must_lock = true;
663 break;
668 // set the locked flag to this and all linked ones
669 if (must_lock && !locked) {
670 setLocked(true);
671 return true;
674 return false;
677 /** Returns the layer of lowest Z coordinate where this ZDisplayable has a point in, or the creation layer if no points yet. */
678 @Override
679 public Layer getFirstLayer() {
680 if (0 == n_points) return this.layer;
681 if (-1 == n_points) setupForDisplay(); //reload
682 Layer la = this.layer;
683 final double z = Double.MAX_VALUE;
684 for (int i=0; i<n_points; i++) {
685 final Layer layer = layer_set.getLayer(p_layer[i]);
686 if (layer.getZ() < z) la = layer;
688 return la;
691 /** Returns the raw data for the balls, sorted by Layer ID versus double[]{z,y,r} . */
692 public Map<Layer,double[]> getRawBalls() {
693 if (-1 == n_points) setupForDisplay(); // reload
694 final HashMap<Layer,double[]> m = new HashMap<Layer,double[]>();
695 for (int i=0; i<n_points; i++) {
696 m.put(layer_set.getLayer(p_layer[i]), new double[]{p[0][i], p[1][i], p_width[i]});
698 return m;
701 /** Returns a [n_points][4] array, with x,y,z,radius on the second part; not transformed, but local!
702 * To obtain balls in world coordinates, calibrated, use getWorldBalls().
704 public double[][] getBalls() {
705 if (-1 == n_points) setupForDisplay(); // reload
706 final double[][] b = new double[n_points][4];
707 for (int i=0; i<n_points; i++) {
708 b[i][0] = p[0][i];
709 b[i][1] = p[1][i];
710 b[i][2] = layer_set.getLayer(p_layer[i]).getZ();
711 b[i][3] = p_width[i];
713 return b;
716 /** 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). */
717 public double[][] getWorldBalls() {
718 if (-1 == n_points) setupForDisplay(); // reload
719 final double[][] b = new double[n_points][4];
720 final Calibration cal = getLayerSet().getCalibrationCopy();
721 final int sign = cal.pixelDepth < 0 ? -1 : 1;
722 for (int i=0; i<n_points; i++) {
723 final Point2D.Double po = transformPoint(p[0][i], p[1][i]); // bring to world coordinates
724 b[i][0] = po.x * cal.pixelWidth;
725 b[i][1] = po.y * cal.pixelHeight;
726 b[i][2] = layer_set.getLayer(p_layer[i]).getZ() * cal.pixelWidth * sign;
727 b[i][3] = p_width[i] * cal.pixelWidth;
729 return b;
732 /** Returns a Point3f for every x,y,z ball, in calibrated world space. */
733 public List<Point3f> asWorldPoints() {
734 final ArrayList<Point3f> ps = new ArrayList<Point3f>();
735 for (final double[] d : getWorldBalls()) {
736 ps.add(new Point3f((float)d[0], (float)d[1], (float)d[2]));
738 return ps;
741 @Override
742 public void exportSVG(final StringBuffer data, final double z_scale, final String indent) {
743 if (-1 == n_points) setupForDisplay(); // reload
744 if (0 == n_points) return;
745 final String in = indent + "\t";
746 final String[] RGB = Utils.getHexRGBColor(color);
747 final double[] a = new double[6];
748 at.getMatrix(a);
749 data.append(indent).append("<ball_ob\n>")
750 .append(in).append("id=\"").append(id).append("\"")
751 .append(in).append("transform=\"matrix(").append(a[0]).append(',')
752 .append(a[1]).append(',')
753 .append(a[2]).append(',')
754 .append(a[3]).append(',')
755 .append(a[4]).append(',')
756 .append(a[5]).append(")\"\n")
757 .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")
758 .append(in).append("links=\"")
760 if (null != hs_linked && 0 != hs_linked.size()) {
761 int ii = 0;
762 final int len = hs_linked.size();
763 for (final Displayable d : hs_linked) {
764 data.append(d.getId());
765 if (ii != len-1) data.append(",");
766 ii++;
769 data.append("\"\n")
770 .append(indent).append(">\n");
771 for (int i=0; i<n_points; i++) {
772 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");
774 data.append(indent).append("</ball_ob>\n");
777 /** 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. */
778 @Override
779 public void exportXML(final StringBuilder sb_body, final String indent, final XMLOptions options) {
780 if (-1 == n_points) setupForDisplay(); // reload
781 //if (0 == n_points) return;
782 final String in = indent + "\t";
783 final String[] RGB = Utils.getHexRGBColor(color);
784 sb_body.append(indent).append("<t2_ball\n");
785 super.exportXML(sb_body, in, options);
786 if (!fill_paint) sb_body.append(in).append("fill=\"").append(fill_paint).append("\"\n"); // otherwise no need
787 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")
789 sb_body.append(indent).append(">\n");
790 for (int i=0; i<n_points; i++) {
791 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");
793 super.restXML(sb_body, in, options);
794 sb_body.append(indent).append("</t2_ball>\n");
797 static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
798 final String type = "t2_ball";
799 if (hs.contains(type)) return;
800 hs.add(type);
801 sb_header.append(indent).append("<!ELEMENT t2_ball (").append(Displayable.commonDTDChildren()).append(",t2_ball_ob)>\n");
802 Displayable.exportDTD(type, sb_header, hs, indent);
803 sb_header.append(indent).append("<!ATTLIST t2_ball fill NMTOKEN #REQUIRED>\n")
804 .append(indent).append("<!ELEMENT t2_ball_ob EMPTY>\n")
805 .append(indent).append("<!ATTLIST t2_ball_ob x NMTOKEN #REQUIRED>\n")
806 .append(indent).append("<!ATTLIST t2_ball_ob y NMTOKEN #REQUIRED>\n")
807 .append(indent).append("<!ATTLIST t2_ball_ob r NMTOKEN #REQUIRED>\n")
808 .append(indent).append("<!ATTLIST t2_ball_ob layer_id NMTOKEN #REQUIRED>\n")
812 /** */ // this may be inaccurate
813 @Override
814 public boolean paintsAt(final Layer layer) {
815 if (!super.paintsAt(layer)) return false;
816 // find previous and next
817 final long lid_previous = layer_set.previous(layer).getId(); // never null, may be the same though
818 final long lid_next = layer_set.next(layer).getId(); // idem
819 final long lid = layer.getId();
820 for (int i=0; i<p_layer.length; i++) {
821 if (lid == p_layer[i] || lid_previous == p_layer[i] || lid_next == p_layer[i]) return true;
823 return false;
826 /** Returns information on the number of ball objects per layer. */
827 @Override
828 public String getInfo() {
829 // group balls by layer
830 final HashMap<Long,ArrayList<Integer>> ht = new HashMap<Long,ArrayList<Integer>>();
831 for (int i=0; i<n_points; i++) {
832 ArrayList<Integer> al = ht.get(new Long(p_layer[i]));
833 if (null == al) {
834 al = new ArrayList<Integer>();
835 ht.put(p_layer[i], al);
837 al.add(i);
839 int total = 0;
840 final StringBuilder sb1 = new StringBuilder("Ball id: ").append(this.id).append('\n');
841 final StringBuilder sb = new StringBuilder();
842 for (final Map.Entry<Long,ArrayList<Integer>> entry : ht.entrySet()) {
843 final long lid = entry.getKey().longValue();
844 final ArrayList<Integer> al = entry.getValue();
845 sb.append("\tLayer ").append(this.layer_set.getLayer(lid).toString()).append(":\n");
846 sb.append("\t\tcount : ").append(al.size()).append('\n');
847 total += al.size();
848 double average = 0;
849 for (final Integer i : al) {
850 average += p_width[i.intValue()];
852 sb.append("\t\taverage radius: ").append(average / al.size()).append('\n');
854 return sb1.append("Total count: ").append(total).append('\n').append(sb).toString();
857 /** Performs a deep copy of this object, without the links. */
858 @Override
859 public Displayable clone(final Project pr, final boolean copy_id) {
860 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
861 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());
862 // links are left null
863 // The data:
864 if (-1 == n_points) setupForDisplay(); // load data
865 copy.n_points = n_points;
866 copy.p = new double[][]{(double[])this.p[0].clone(), (double[])this.p[1].clone()};
867 copy.p_layer = (long[])this.p_layer.clone();
868 copy.p_width = (double[])this.p_width.clone();
869 copy.addToDatabase();
870 return copy;
874 /** 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.*/
875 static public double[][][] generateGlobe(int meridians, int parallels) {
876 if (meridians < 3) meridians = 3;
877 if (parallels < 3) parallels = 3;
878 /* to do: 2 loops:
879 -first loop makes horizontal circle using meridian points.
880 -second loop scales it appropiately and makes parallels.
881 Both loops are common for all balls and so should be done just once.
882 Then this globe can be properly translocated and resized for each ball.
884 // a circle of radius 1
885 double angle_increase = 2*Math.PI / meridians;
886 double temp_angle = 0;
887 final double[][] xy_points = new double[meridians+1][2]; //plus 1 to repeat last point
888 xy_points[0][0] = 1; // first point
889 xy_points[0][1] = 0;
890 for (int m=1; m<meridians; m++) {
891 temp_angle = angle_increase*m;
892 xy_points[m][0] = Math.cos(temp_angle);
893 xy_points[m][1] = Math.sin(temp_angle);
895 xy_points[xy_points.length-1][0] = 1; // last point
896 xy_points[xy_points.length-1][1] = 0;
898 // Build parallels from circle
899 angle_increase = Math.PI / parallels; // = 180 / parallels in radians
900 //final double angle90 = Math.toRadians(90);
901 final double[][][] xyz = new double[parallels+1][xy_points.length][3];
902 for (int p=1; p<xyz.length-1; p++) {
903 final double radius = Math.sin(angle_increase*p);
904 final double Z = Math.cos(angle_increase*p);
905 for (int mm=0; mm<xyz[0].length-1; mm++) {
906 //scaling circle to appropriate radius, and positioning the Z
907 xyz[p][mm][0] = xy_points[mm][0] * radius;
908 xyz[p][mm][1] = xy_points[mm][1] * radius;
909 xyz[p][mm][2] = Z;
911 xyz[p][xyz[0].length-1][0] = xyz[p][0][0]; //last one equals first one
912 xyz[p][xyz[0].length-1][1] = xyz[p][0][1];
913 xyz[p][xyz[0].length-1][2] = xyz[p][0][2];
916 // south and north poles
917 for (int ns=0; ns<xyz[0].length; ns++) {
918 xyz[0][ns][0] = 0; //south pole
919 xyz[0][ns][1] = 0;
920 xyz[0][ns][2] = 1;
921 xyz[xyz.length-1][ns][0] = 0; //north pole
922 xyz[xyz.length-1][ns][1] = 0;
923 xyz[xyz.length-1][ns][2] = -1;
926 return xyz;
930 /** 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.*/
931 public List<Point3f> generateTriangles(final double scale, final double[][][] globe) {
932 try {
933 Class.forName("javax.vecmath.Point3f");
934 } catch (final ClassNotFoundException cnfe) {
935 Utils.log("Java3D is not installed.");
936 return null;
938 final Calibration cal = layer_set.getCalibrationCopy();
939 // modify the globe to fit each ball's radius and x,y,z position
940 final ArrayList<Point3f> list = new ArrayList<Point3f>();
941 // transform points
942 // local pointers, since they may be transformed
943 double[][] p = this.p;
944 double[] p_width = this.p_width;
945 if (!this.at.isIdentity()) {
946 final Object[] ob = getTransformedData();
947 p = (double[][])ob[0];
948 p_width = (double[])ob[1];
950 final int sign = cal.pixelDepth < 0 ? -1 : 1;
951 // for each ball
952 for (int i=0; i<n_points; i++) {
953 // create local globe for the ball, and translate it to z,y,z
954 final double[][][] ball = new double[globe.length][globe[0].length][3];
955 for (int z=0; z<ball.length; z++) {
956 for (int k=0; k<ball[0].length; k++) {
957 // 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.
958 ball[z][k][0] = (globe[z][k][0] * p_width[i] + p[0][i]) * scale * cal.pixelWidth;
959 ball[z][k][1] = (globe[z][k][1] * p_width[i] + p[1][i]) * scale * cal.pixelHeight;
960 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.
963 // create triangular faces and add them to the list
964 for (int z=0; z<ball.length-1; z++) { // the parallels
965 for (int k=0; k<ball[0].length -1; k++) { // meridian points
966 // half quadrant (a triangle)
967 list.add(new Point3f((float)ball[z][k][0], (float)ball[z][k][1], (float)ball[z][k][2]));
968 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]));
969 list.add(new Point3f((float)ball[z+1][k][0], (float)ball[z+1][k][1], (float)ball[z+1][k][2]));
970 // the other half quadrant
971 list.add(new Point3f((float)ball[z][k][0], (float)ball[z][k][1], (float)ball[z][k][2]));
972 list.add(new Point3f((float)ball[z][k+1][0], (float)ball[z][k+1][1], (float)ball[z][k+1][2]));
973 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]));
975 // 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
978 return list;
982 private final Object[] getTransformedData() {
983 return getTransformedData(null);
986 /** Apply the AffineTransform to a copy of the points and return the arrays. */
987 private final Object[] getTransformedData(final AffineTransform additional) {
988 // transform points
989 final double[][] p = transformPoints(this.p, additional);
990 // 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])
991 double[][] pw = new double[2][n_points];
992 for (int i=0; i<n_points; i++) {
993 pw[0][i] = this.p[0][i] + p_width[i]; //built relative to the untransformed points!
994 pw[1][i] = this.p[1][i] + p_width[i];
996 pw = transformPoints(pw, additional);
997 final double[] p_width = new double[n_points];
998 for (int i=0; i<n_points; i++) {
999 // plain average of differences in X and Y axis, relative to the transformed points.
1000 p_width[i] = (Math.abs(pw[0][i] - p[0][i]) + Math.abs(pw[1][i] - p[1][i])) / 2;
1002 return new Object[]{p, p_width};
1005 /** @param area is expected in world coordinates. */
1006 @Override
1007 public boolean intersects(final Area area, final double z_first, final double z_last) {
1008 // find lowest and highest Z
1009 double min_z = Double.MAX_VALUE;
1010 double max_z = 0;
1011 for (int i=0; i<n_points; i++) {
1012 final double laz =layer_set.getLayer(p_layer[i]).getZ();
1013 if (laz < min_z) min_z = laz;
1014 if (laz > max_z) max_z = laz;
1016 if (z_last < min_z || z_first > max_z) return false;
1017 // check the roi
1018 for (int i=0; i<n_points; i++) {
1019 final Rectangle[] rec = getSubPerimeters(layer_set.getLayer(p_layer[i]));
1020 for (int k=0; k<rec.length; k++) {
1021 final Area a = new Area(rec[k]); // subperimeters already in world coords
1022 a.intersect(area);
1023 final Rectangle r = a.getBounds();
1024 if (0 != r.width && 0 != r.height) return true;
1027 return false;
1030 @Override
1031 synchronized public Area getAreaAt(final Layer layer) {
1032 final Area a = new Area();
1033 for (int i=0; i<n_points; i++) {
1034 if (p_layer[i] != layer.getId()) continue;
1035 a.add(new Area(new Ellipse2D.Float((float)(p[0][i] - p_width[i]/2), (float)(p[1][i] - p_width[i]/2), (float)p_width[i], (float)p_width[i])));
1037 a.transform(this.at);
1038 return a;
1041 @Override
1042 protected boolean isRoughlyInside(final Layer layer, final Rectangle r) {
1043 if (0 == n_points) return false;
1044 try {
1045 final Rectangle box = this.at.createInverse().createTransformedShape(r).getBounds();
1046 for (int i=0; i<n_points; i++) {
1047 if (box.contains(p[0][i], p[1][i])) return true;
1049 } catch (final NoninvertibleTransformException nite) {
1050 IJError.print(nite);
1052 return false;
1055 /** Returns a listing of all balls contained here, one per row with index, x, y, z, and radius, all calibrated.
1056 * 'name-id' is a column that displays the title of this Ball object only when such title is purely a number.
1058 @Override
1059 public ResultsTable measure(ResultsTable rt) {
1060 if (-1 == n_points) setupForDisplay(); //reload
1061 if (0 == n_points) return rt;
1062 if (null == rt) rt = Utils.createResultsTable("Ball results", new String[]{"id", "index", "x", "y", "z", "radius", "name-id"});
1063 final Object[] ob = getTransformedData();
1064 final double[][] p = (double[][])ob[0];
1065 final double[] p_width = (double[])ob[1];
1066 final Calibration cal = layer_set.getCalibration();
1067 for (int i=0; i<n_points; i++) {
1068 rt.incrementCounter();
1069 rt.addLabel("units", cal.getUnit());
1070 rt.addValue(0, this.id);
1071 rt.addValue(1, i+1);
1072 rt.addValue(2, p[0][i] * cal.pixelWidth);
1073 rt.addValue(3, p[1][i] * cal.pixelHeight);
1074 rt.addValue(4, layer_set.getLayer(p_layer[i]).getZ() * cal.pixelWidth);
1075 rt.addValue(5, p_width[i] * cal.pixelWidth);
1076 rt.addValue(6, getNameId());
1078 return rt;
1081 @Override
1082 Class<?> getInternalDataPackageClass() {
1083 return DPBall.class;
1086 @Override
1087 Object getDataPackage() {
1088 return new DPBall(this);
1091 static private final class DPBall extends Displayable.DataPackage {
1092 final double[][] p;
1093 final double[] p_width;
1094 final long[] p_layer;
1096 DPBall(final Ball ball) {
1097 super(ball);
1098 // store copies of all arrays
1099 this.p = new double[][]{Utils.copy(ball.p[0], ball.n_points), Utils.copy(ball.p[1], ball.n_points)};
1100 this.p_width = Utils.copy(ball.p_width, ball.n_points);
1101 this.p_layer = new long[ball.n_points]; System.arraycopy(ball.p_layer, 0, this.p_layer, 0, ball.n_points);
1103 @Override
1104 final boolean to2(final Displayable d) {
1105 super.to1(d);
1106 final Ball ball = (Ball)d;
1107 final int len = p[0].length; // == n_points, since it was cropped on copy
1108 ball.p = new double[][]{Utils.copy(p[0], len), Utils.copy(p[1], len)};
1109 ball.n_points = p[0].length;
1110 ball.p_layer = new long[len]; System.arraycopy(p_layer, 0, ball.p_layer, 0, len);
1111 ball.p_width = Utils.copy(p_width, len);
1112 return true;
1116 /** Retain the data within the layer range, and throw out all the rest. */
1117 @Override
1118 synchronized public boolean crop(final List<Layer> range) {
1119 if (-1 == n_points) setupForDisplay();
1120 final HashSet<Long> lids = new HashSet<Long>();
1121 for (final Layer l : range) {
1122 lids.add(l.getId());
1124 for (int i=0; i<n_points; i++) {
1125 if (!lids.contains(p_layer[i])) {
1126 removePoint(i);
1127 i--;
1130 calculateBoundingBox(true, null);
1131 return true;
1134 @Override
1135 synchronized protected boolean layerRemoved(final Layer la) {
1136 super.layerRemoved(la);
1137 for (int i=0; i<p_layer.length; i++) {
1138 if (la.getId() == p_layer[i]) {
1139 removePoint(i);
1140 i--;
1143 return true;
1146 @Override
1147 synchronized public boolean apply(final Layer la, final Area roi, final mpicbg.models.CoordinateTransform ict) throws Exception {
1148 double[] fp = null;
1149 mpicbg.models.CoordinateTransform chain = null;
1150 Area localroi = null;
1151 AffineTransform inverse = null;
1152 for (int i=0; i<n_points; i++) {
1153 if (p_layer[i] == la.getId()) {
1154 if (null == localroi) {
1155 inverse = this.at.createInverse();
1156 localroi = roi.createTransformedArea(inverse);
1158 if (localroi.contains(p[0][i], p[1][i])) {
1159 if (null == chain) {
1160 chain = M.wrap(this.at, ict, inverse);
1161 fp = new double[2];
1163 // Keep point copy
1164 final double ox = p[0][i],
1165 oy = p[1][i];
1166 // Transform the point
1167 M.apply(chain, p, i, fp);
1168 // For radius, assume it's a point to the right of the center point
1169 fp[0] = (float)(ox + p_width[i]);
1170 fp[1] = (float)oy;
1171 chain.applyInPlace(fp);
1172 p_width[i] = Math.abs(fp[0] - p[0][i]);
1176 if (null != chain) calculateBoundingBox(true, la); // may be called way too many times, but avoids lots of headaches.
1177 return true;
1179 @Override
1180 public boolean apply(final VectorDataTransform vdt) throws Exception {
1181 final double[] fp = new double[2];
1182 final VectorDataTransform vlocal = vdt.makeLocalTo(this);
1183 for (int i=0; i<n_points; i++) {
1184 if (vlocal.layer.getId() == p_layer[i]) {
1185 for (final VectorDataTransform.ROITransform rt : vlocal.transforms) {
1186 if (rt.roi.contains(p[0][i], p[1][i])) {
1187 // Keep point copy
1188 final double ox = p[0][i],
1189 oy = p[1][i];
1190 // Transform the point
1191 M.apply(rt.ct, p, i, fp);
1192 // For radius, assume it's a point to the right of the center point
1193 fp[0] = (float)(ox + p_width[i]);
1194 fp[1] = (float)oy;
1195 rt.ct.applyInPlace(fp);
1196 p_width[i] = Math.sqrt(Math.pow(fp[0] - p[0][i], 2) + Math.pow(fp[1] - p[1][i], 2));
1197 break;
1202 calculateBoundingBox(true, vlocal.layer);
1203 return true;
1206 @Override
1207 synchronized public Collection<Long> getLayerIds() {
1208 return Utils.asList(p_layer, 0, n_points);
1211 @Override
1212 public void adjustProperties() {
1213 final GenericDialog gd = makeAdjustPropertiesDialog(); // in superclass
1214 gd.addCheckbox("Paint as outlines", !fill_paint);
1215 gd.addCheckbox("Apply paint mode to all Ball instances", false);
1216 gd.showDialog();
1217 if (gd.wasCanceled()) return;
1218 // superclass processing
1219 final Displayable.DoEdit prev = processAdjustPropertiesDialog(gd);
1220 // local proccesing
1221 final boolean fp = !gd.getNextBoolean();
1222 final boolean to_all = gd.getNextBoolean();
1223 if (to_all) {
1224 for (final ZDisplayable zd : layer_set.getZDisplayables()) {
1225 if (zd.getClass() == Ball.class) {
1226 final Ball b = (Ball)zd;
1227 b.fill_paint = fp;
1228 b.updateInDatabase("fill_paint");
1231 Display.repaint(layer_set);
1232 } else if (fill_paint != fp) {
1233 prev.add("fill_paint", fp);
1234 this.fill_paint = fp; // change it after storing state in DoEdit
1235 updateInDatabase("fill_paint");
1238 // Add current step, with the same modified keys
1239 final DoEdit current = new DoEdit(this).init(prev);
1240 if (isLinked()) current.add(new Displayable.DoTransforms().addAll(getLinkedGroup(null)));
1241 getLayerSet().addEditStep(current);
1244 /** Set the x,y,radius raw pixel values for the ball at index i.
1245 * When done setting values, call repaint(true, null).
1246 * @throws IndexOutOfBoundsException if i &lt; 0 or i &gt;= the number of points. */
1247 public void set(final int i, final double x, final double y, final Layer la, final double radius) {
1248 if (i < 0 || i > n_points) throw new IndexOutOfBoundsException("i must be 0<=i<n_points, but it is " + i);
1249 p[0][i] = x;
1250 p[1][i] = y;
1251 p_layer[i] = la.getId();
1252 p_width[i] = radius;
1255 /** Return the number of balls. */
1256 public int getCount() {
1257 return n_points;
1260 /** Set the radius (raw pixel value) for the ball at index i.
1261 * When done setting values, call repaint(true, null).
1262 * @throws IndexOutOfBoundsException if i &lt; 0 or i &gt;= the number of points. */
1263 public void setRadius(final int i, final double radius) {
1264 if (i < 0 || i > n_points) throw new IndexOutOfBoundsException("i must be 0<=i<n_points, but it is " + i);
1265 p_width[i] = radius;