Major cleanup of Utils class.
[trakem2.git] / ini / trakem2 / display / Ball.java
blobb98bfa6305f16f2193d891d985d52b004a322ae8
1 /**
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.
21 **/
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;
35 import java.awt.*;
36 import java.awt.event.MouseEvent;
37 import java.awt.event.KeyEvent;
38 import java.util.ArrayList;
39 import java.util.Map;
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);
66 n_points = 0;
67 p = new double[2][5];
68 p_layer = new long[5]; // the ids of the layers in which each point lays
69 p_width = new double[5];
70 addToDatabase();
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;
77 this.alpha = alpha;
78 this.color = color;
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
86 this.n_points = 0;
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();
95 p[0][n_points] = x;
96 p[1][n_points] = y;
97 p_width[n_points] = r;
98 p_layer[n_points] = layer_id;
99 n_points++;
102 /**Increase the size of the arrays by 5.*/
103 private void enlargeArrays() {
104 //catch length
105 int length = p[0].length;
106 //make copies
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];
110 //copy values
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);
115 //assign them
116 this.p = p_copy;
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) {
123 int index = -1;
124 double d = (10.0D / magnification);
125 if (d < 4) d = 4;
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]) {
128 index = i;
131 return index;
133 /**Remove a point.*/
134 protected void removePoint(int index) {
135 // check preconditions:
136 if (index < 0) {
137 return;
138 } else if (n_points - 1 == index) {
139 //last point out
140 n_points--;
141 } else {
142 //one point out (but not the last)
143 --n_points;
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.
148 p[1][i] = p[1][i+1];
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);
156 //update in database
157 updateInDatabase("points");
160 /**Move backbone point by the given deltas.*/
161 private void dragPoint(int index, int dx, int dy) {
162 p[0][index] += dx;
163 p[1][index] += 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
175 //check array size
176 if (p[0].length == n_points) {
177 enlargeArrays();
179 //append at the end
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
184 index = n_points;
185 //add one up
186 this.n_points++;
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());
188 return index;
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
195 setupForDisplay();
197 //arrange transparency
198 Composite original_composite = null;
199 if (alpha != 1.0f) {
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"));
216 // paint proper:
217 final int i_current = layer_set.getLayerIndex(active_layer.getId());
218 int ii;
219 int radius;
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);
229 if (active) {
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) {
244 // TODO
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);
254 x_p = (int)po.x;
255 y_p = (int)po.y;
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
264 } else {
265 index = findPoint(p, x_p, y_p, mag);
267 if (-1 != index) {
268 if (layer_id == p_layer[index]) {
269 if (Utils.isControlDown(me) && me.isShiftDown() && p_layer[index] == Display.getFrontLayer().getId()) {
270 removePoint(index);
271 index = -1; // to prevent saving in the database twice
272 repaint(false);
273 return;
275 } else index = -1; // disable if not in the front layer (so a new point will be added)
276 if (-1 != index) {
277 // Make the radius for newly added balls that of the last selected
278 last_radius = p_width[index];
281 if (-1 == index) {
282 index = addPoint(x_p, y_p, mag, layer_id);
283 repaint(false);
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);
292 x_p = (int)p.x;
293 y_p = (int)p.y;
294 final Point2D.Double pd = inverseTransformPoint(x_d, y_d);
295 x_d = (int)pd.x;
296 y_d = (int)pd.y;
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) {
305 if (-1 != index) {
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);
310 } else {
311 dragPoint(index, x_d - x_d_old, y_d - y_d_old);
313 repaint(false);
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
325 if (-1 != index) {
326 //later!//calculateBoundingBox(true);
327 updateInDatabase("transform+dimensions");
330 // reset
331 index = -1;
332 repaint(true);
335 private void calculateBoundingBox(boolean adjust_position) {
336 double min_x = Double.MAX_VALUE;
337 double min_y = Double.MAX_VALUE;
338 double max_x = 0.0D;
339 double max_y = 0.0D;
340 if (0 == n_points) {
341 this.width = this.height = 0;
342 layer_set.updateBucket(this);
343 return;
345 if (0 != n_points) {
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");
363 } else {
364 updateInDatabase("dimensions");
367 layer_set.updateBucket(this);
370 /**Release all memory resources taken by this object.*/
371 public void destroy() {
372 super.destroy();
373 p = null;
374 p_layer = null;
375 p_width = null;
379 public void repaint() {
380 repaint(true);
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() {
394 // load points
395 if (null == p) {
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();
402 int i = 0;
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();
409 i++;
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() {
415 p = null;
416 p_width = null;
417 p_layer = null;
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];
434 if (-1 != index) {
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);
437 } else {
438 // the whole box
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();
447 final char l = '\n';
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)
463 .append("in slice=")
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();
476 if (null == tmp) {
477 //else {
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)
486 tmp = null;
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(",")
505 .append(p_layer[i])
506 .append(")");
507 ; //end
508 sql[i] = sb.toString();
510 return sql;
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)
522 ; //end
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;
534 // make x,y local
535 final Point2D.Double po = inverseTransformPoint(x, y);
536 x = (int)po.x;
537 y = (int)po.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;
544 return false;
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;
563 else {
564 final Rectangle[] rects = new Rectangle[al.size()];
565 al.toArray(rects);
566 return rects;
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))) {
590 // Link the patch
591 this.link(displ);
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;
607 return la;
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++) {
617 b[i][0] = p[0][i];
618 b[i][1] = p[1][i];
619 b[i][2] = layer_set.getLayer(p_layer[i]).getZ();
620 b[i][3] = p_width[i];
622 return b;
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;
638 return b;
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];
647 at.getMatrix(a);
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()) {
660 int ii = 0;
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(",");
666 ii++;
669 data.append("\"\n")
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;
698 hs.add(type);
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;
719 return false;
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]));
728 if (null == al) {
729 al = new ArrayList();
730 ht.put(new Long(p_layer[i]), al);
732 al.add(new Integer(i)); // blankets!
734 int total = 0;
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');
743 total += al.size();
744 double average = 0;
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
759 // The data:
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();
766 return copy;
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;
774 /* to do: 2 loops:
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
785 xy_points[0][1] = 0;
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;
805 xyz[p][mm][2] = Z;
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
815 xyz[0][ns][1] = 0;
816 xyz[0][ns][2] = 1;
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;
822 return xyz;
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) {
828 try {
829 Class c = Class.forName("javax.vecmath.Point3f");
830 } catch (ClassNotFoundException cnfe) {
831 Utils.log("Java3D is not installed.");
832 return null;
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();
837 // transform points
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;
847 // for each ball
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
874 return list;
877 /** Apply the AffineTransform to a copy of the points and return the arrays. */
878 private final Object[] getTransformedData() {
879 // transform points
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;
900 double max_z = 0;
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;
907 // check the roi
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);
912 a.intersect(area);
913 Rectangle r = a.getBounds();
914 if (0 != r.width && 0 != r.height) return true;
917 return false;
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.
923 * */
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);
936 rt.addValue(1, i+1);
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());
943 return rt;
946 @Override
947 Class getInternalDataPackageClass() {
948 return DPBall.class;
951 @Override
952 Object getDataPackage() {
953 return new DPBall(this);
956 static private final class DPBall extends Displayable.DataPackage {
957 final double[][] p;
958 final double[] p_width;
959 final long[] p_layer;
961 DPBall(final Ball ball) {
962 super(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) {
969 super.to1(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);
976 return true;