preparing release pom-trakem2-2.0.0, VectorString-2.0.0, TrakEM2_-1.0h
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / Treeline.java
blobac77426ac44ffe2c66ef0157adf11a0e987377ac
1 package ini.trakem2.display;
3 import java.awt.AlphaComposite;
4 import java.awt.Choice;
5 import java.awt.Color;
6 import java.awt.Composite;
7 import java.awt.Graphics2D;
8 import java.awt.Point;
9 import java.awt.Polygon;
10 import java.awt.Rectangle;
11 import java.awt.Shape;
12 import java.awt.TextField;
13 import java.awt.event.ItemEvent;
14 import java.awt.event.ItemListener;
15 import java.awt.event.KeyEvent;
16 import java.awt.event.MouseEvent;
17 import java.awt.event.MouseWheelEvent;
18 import java.awt.geom.AffineTransform;
19 import java.awt.geom.Area;
20 import java.awt.geom.Ellipse2D;
21 import java.awt.geom.Point2D;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Set;
30 import org.scijava.java3d.Transform3D;
31 import org.scijava.vecmath.AxisAngle4f;
32 import org.scijava.vecmath.Color3f;
33 import org.scijava.vecmath.Point3f;
34 import org.scijava.vecmath.Vector3f;
36 import ij.gui.GenericDialog;
37 import ij.measure.Calibration;
38 import ij.measure.ResultsTable;
39 import ini.trakem2.Project;
40 import ini.trakem2.utils.IJError;
41 import ini.trakem2.utils.M;
42 import ini.trakem2.utils.ProjectToolbar;
43 import ini.trakem2.utils.Utils;
45 public class Treeline extends Tree<Float> {
47 static protected float last_radius = -1;
49 public Treeline(final Project project, final String title) {
50 super(project, title);
51 addToDatabase();
54 /** Reconstruct from XML. */
55 public Treeline(final Project project, final long id, final HashMap<String,String> ht_attr, final HashMap<Displayable,String> ht_links) {
56 super(project, id, ht_attr, ht_links);
59 /** For cloning purposes, does not call addToDatabase() */
60 public Treeline(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) {
61 super(project, id, title, width, height, alpha, visible, color, locked, at);
64 @Override
65 public Tree<Float> newInstance() {
66 return new Treeline(project, project.getLoader().getNextId(), title, width, height, alpha, visible, color, locked, at);
69 @Override
70 public Node<Float> newNode(final float lx, final float ly, final Layer la, final Node<?> modelNode) {
71 return new RadiusNode(lx, ly, la, null == modelNode ? 0 : ((RadiusNode)modelNode).r);
74 @Override
75 public Node<Float> newNode(final HashMap<String,String> ht_attr) {
76 return new RadiusNode(ht_attr);
79 @Override
80 public Treeline clone(final Project pr, final boolean copy_id) {
81 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
82 final Treeline tline = new Treeline(pr, nid, title, width, height, alpha, visible, color, locked, at);
83 tline.root = null == this.root ? null : this.root.clone(pr);
84 tline.addToDatabase();
85 if (null != tline.root) tline.cacheSubtree(tline.root.getSubtreeNodes());
86 return tline;
89 @Override
90 public void mousePressed(final MouseEvent me, final Layer la, final int x_p, final int y_p, final double mag) {
91 if (-1 == last_radius) {
92 last_radius = 10 / (float)mag;
95 if (me.isShiftDown() && me.isAltDown() && !Utils.isControlDown(me)) {
96 final Display front = Display.getFront(this.project);
97 final Layer layer = front.getLayer();
98 final Node<Float> nd = findNodeNear(x_p, y_p, layer, front.getCanvas());
99 if (null == nd) {
100 Utils.log("Can't adjust radius: found more than 1 node within visible area!");
101 return;
103 // So: only one node within visible area of the canvas:
104 // Adjust the radius by shift+alt+drag
106 float xp = x_p,
107 yp = y_p;
108 if (!this.at.isIdentity()) {
109 final Point2D.Double po = inverseTransformPoint(x_p, y_p);
110 xp = (int)po.x;
111 yp = (int)po.y;
114 setActive(nd);
115 nd.setData((float)Math.sqrt(Math.pow(xp - nd.x, 2) + Math.pow(yp - nd.y, 2)));
116 repaint(true, la);
117 setLastEdited(nd);
119 return;
122 super.mousePressed(me, la, x_p, y_p, mag);
125 protected boolean requireAltDownToEditRadius() {
126 return true;
129 @Override
130 public void mouseDragged(final MouseEvent me, final Layer la, final int x_p, final int y_p, final int x_d, final int y_d, final int x_d_old, final int y_d_old) {
131 if (null == getActive()) return;
133 if (requireAltDownToEditRadius() && !me.isAltDown()) {
134 super.mouseDragged(me, la, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
135 return;
137 if (me.isShiftDown() && !Utils.isControlDown(me)) {
138 // transform to the local coordinates
139 float xd = x_d,
140 yd = y_d;
141 if (!this.at.isIdentity()) {
142 final Point2D.Double po = inverseTransformPoint(x_d, y_d);
143 xd = (float)po.x;
144 yd = (float)po.y;
146 final Node<Float> nd = getActive();
147 final float r = (float)Math.sqrt(Math.pow(xd - nd.x, 2) + Math.pow(yd - nd.y, 2));
148 nd.setData(r);
149 last_radius = r;
150 repaint(true, la);
151 return;
154 super.mouseDragged(me, la, x_p, y_p, x_d, y_d, x_d_old, y_d_old);
157 @Override
158 public void mouseReleased(final MouseEvent me, final Layer la, final int x_p, final int y_p, final int x_d, final int y_d, final int x_r, final int y_r) {
159 if (null == getActive()) return;
161 if (me.isShiftDown() && me.isAltDown() && !Utils.isControlDown(me)) {
162 updateViewData(getActive());
163 return;
165 super.mouseReleased(me, la, x_p, y_p, x_d, y_d, x_r, y_r);
168 @Override
169 public void mouseWheelMoved(final MouseWheelEvent mwe) {
170 final int modifiers = mwe.getModifiers();
171 if (0 == ( (MouseWheelEvent.SHIFT_MASK | MouseWheelEvent.ALT_MASK) ^ modifiers)) {
172 final Object source = mwe.getSource();
173 if (! (source instanceof DisplayCanvas)) return;
174 final DisplayCanvas dc = (DisplayCanvas)source;
175 final Layer la = dc.getDisplay().getLayer();
176 final int rotation = mwe.getWheelRotation();
177 final float magnification = (float)dc.getMagnification();
178 final Rectangle srcRect = dc.getSrcRect();
179 final float x = ((mwe.getX() / magnification) + srcRect.x);
180 final float y = ((mwe.getY() / magnification) + srcRect.y);
182 final float inc = (rotation > 0 ? 1 : -1) * (1/magnification);
183 if (null != adjustNodeRadius(inc, x, y, la, dc)) {
184 Display.repaint(this);
185 mwe.consume();
186 return;
189 super.mouseWheelMoved(mwe);
192 protected Node<Float> adjustNodeRadius(final float inc, final float x, final float y, final Layer layer, final DisplayCanvas dc) {
193 final Node<Float> nearest = findNodeNear(x, y, layer, dc);
194 if (null == nearest) {
195 Utils.log("Can't adjust radius: found more than 1 node within visible area!");
196 return null;
198 nearest.setData(nearest.getData() + inc);
199 return nearest;
202 static public class RadiusNode extends Node<Float> {
203 protected float r;
205 public RadiusNode(final float lx, final float ly, final Layer la) {
206 this(lx, ly, la, 0);
208 public RadiusNode(final float lx, final float ly, final Layer la, final float radius) {
209 super(lx, ly, la);
210 this.r = radius;
212 /** To reconstruct from XML, without a layer. */
213 public RadiusNode(final HashMap<String,String> attr) {
214 super(attr);
215 final String sr = (String)attr.get("r");
216 this.r = null == sr ? 0 : Float.parseFloat(sr);
219 @Override
220 public Node<Float> newInstance(final float lx, final float ly, final Layer layer) {
221 return new RadiusNode(lx, ly, layer, 0);
224 /** Set the radius to a positive value. When zero or negative, it's set to zero. */
225 @Override
226 public final boolean setData(final Float radius) {
227 this.r = radius > 0 ? radius : 0;
228 return true;
230 @Override
231 public final Float getData() { return this.r; }
233 @Override
234 public final Float getDataCopy() { return this.r; }
236 @Override
237 public boolean isRoughlyInside(final Rectangle localbox) {
238 if (0 == this.r) {
239 if (null == parent) {
240 return localbox.contains((int)this.x, (int)this.y);
241 } else {
242 if (0 == parent.getData()) { // parent.getData() == ((RadiusNode)parent).r
243 return localbox.intersectsLine(parent.x, parent.y, this.x, this.y);
244 } else {
245 return segmentIntersects(localbox);
248 } else {
249 if (null == parent) {
250 return localbox.contains((int)this.x, (int)this.y);
251 } else {
252 return segmentIntersects(localbox);
257 private final Polygon getSegment() {
258 final RadiusNode parent = (RadiusNode) this.parent;
259 float vx = parent.x - this.x;
260 float vy = parent.y - this.y;
261 final float len = (float) Math.sqrt(vx*vx + vy*vy);
262 if (0 == len) {
263 // Points are on top of each other
264 return new Polygon(new int[]{(int)this.x, (int)Math.ceil(parent.x)},
265 new int[]{(int)this.y, (int)Math.ceil(parent.y)}, 2);
267 vx /= len;
268 vy /= len;
269 // perpendicular vector
270 final float vx90 = -vy;
271 final float vy90 = vx;
272 final float vx270 = vy;
273 final float vy270 = -vx;
275 return new Polygon(new int[]{(int)(parent.x + vx90 * parent.r), (int)(parent.x + vx270 * parent.r), (int)(this.x + vx270 * this.r), (int)(this.x + vx90 * this.r)},
276 new int[]{(int)(parent.y + vy90 * parent.r), (int)(parent.y + vy270 * parent.r), (int)(this.y + vy270 * this.r), (int)(this.y + vy90 * this.r)},
280 // The human compiler at work!
281 /** Detect intersection between localRect and the bounds of getSegment() */
282 private final boolean segmentIntersects(final Rectangle localRect) {
283 final RadiusNode parent = (RadiusNode) this.parent;
284 float vx = parent.x - this.x;
285 float vy = parent.y - this.y;
286 final float len = (float) Math.sqrt(vx*vx + vy*vy);
287 if (0 == len) {
288 // Points are on top of each other
289 return localRect.contains(this.x, this.y);
291 vx /= len;
292 vy /= len;
293 // perpendicular vector
294 //final float vx90 = -vy;
295 //final float vy90 = vx;
296 //final float vx270 = vy;
297 //final float vy270 = -vx;
299 final float x1 = parent.x + (-vy) /*vx90*/ * parent.r,
300 y1 = parent.y + vx /*vy90*/ * parent.r,
301 x2 = parent.x + vy /*vx270*/ * parent.r,
302 y2 = parent.y + (-vx) /*vy270*/ * parent.r,
303 x3 = this.x + vy /*vx270*/ * this.r,
304 y3 = this.y + (-vx) /*vy270*/ * this.r,
305 x4 = this.x + (-vy) /*vx90*/ * this.r,
306 y4 = this.y + vx /*vy90*/ * this.r;
307 final float min_x = Math.min(Math.min(x1, x2), Math.min(x3, x4)),
308 min_y = Math.min(Math.min(y1, y2), Math.min(y3, y4)),
309 max_x = Math.max(Math.max(x1, x2), Math.max(x3, x4)),
310 max_y = Math.max(Math.max(y1, y2), Math.max(y3, y4));
312 final float w = max_x - min_x,
313 h = max_y - min_y;
315 return min_x + w > localRect.x
316 && min_y + h > localRect.y
317 && min_x < localRect.x + localRect.width
318 && min_y < localRect.y + localRect.height;
321 // As above, but inline:
322 return min_x + max_x - min_x > localRect.x
323 && min_y + max_y - min_y > localRect.y
324 && min_x < localRect.x + localRect.width
325 && min_y < localRect.y + localRect.height;
327 // May give false negatives!
328 //return localRect.contains((int)(parent.x + vx90 * parent.r), (int)(parent.y + vy90 * parent.r))
329 // || localRect.contains((int)(parent.x + vx270 * parent.r), (int)(parent.y + vy270 * parent.r))
330 // || localRect.contains((int)(this.x + vx270 * this.r), (int)(this.y + vy270 * this.r))
331 // || localRect.contains((int)(this.x + vx90 * this.r), (int)(this.y + vy90 * this.r));
334 @Override
335 public void paintData(final Graphics2D g, final Rectangle srcRect,
336 final Tree<Float> tree, final AffineTransform to_screen, final Color cc,
337 final Layer active_layer) {
338 if (null == this.parent) return; // doing it here for less total cost
339 if (0 == this.r && 0 == parent.getData()) return;
341 // Two transformations, but it's only 4 points each and it's necessary
342 //final Polygon segment = getSegment();
343 //if (!tree.at.createTransformedShape(segment).intersects(srcRect)) return Node.FALSE;
344 //final Shape shape = to_screen.createTransformedShape(segment);
345 final Shape shape = to_screen.createTransformedShape(getSegment());
346 final Composite c = g.getComposite();
347 final float alpha = tree.getAlpha();
348 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha > 0.4f ? 0.4f : alpha));
349 g.setColor(cc);
350 g.fill(shape);
351 g.setComposite(c);
352 g.draw(shape); // in Tree's composite mode (such as an alpha)
355 /** Expects @param a in local coords. */
356 @Override
357 public boolean intersects(final Area a) {
358 if (0 == r) return a.contains(x, y);
359 return M.intersects(a, new Area(new Ellipse2D.Float(x-r, y-r, r+r, r+r)));
360 // TODO: not the getSegment() ?
363 @Override
364 public void apply(final mpicbg.models.CoordinateTransform ct, final Area roi) {
365 // store the point
366 final double ox = x,
367 oy = y;
368 // transform the point itself
369 super.apply(ct, roi);
370 // transform the radius: assume it's a point to its right
371 if (0 != r) {
372 final double[] fp = new double[]{ox + r, oy};
373 ct.applyInPlace(fp);
374 r = ( float )Math.abs(fp[0] - this.x);
377 @Override
378 public void apply(final VectorDataTransform vdt) {
379 for (final VectorDataTransform.ROITransform rt : vdt.transforms) {
380 // Apply only the first one that contains the point
381 if (rt.roi.contains(x, y)) {
382 // Store point
383 final double ox = x,
384 oy = y;
385 // Transform point
386 final double[] fp = new double[]{x, y};
387 rt.ct.applyInPlace(fp);
388 x = ( float )fp[0];
389 y = ( float )fp[1];
390 // Transform the radius: assume it's a point to the right of the untransformed point
391 if (0 != r) {
392 fp[0] = ox + r;
393 fp[1] = oy;
394 rt.ct.applyInPlace(fp);
395 r = ( float )Math.abs(fp[0] - this.x);
397 break;
402 @Override
403 protected void transformData(final AffineTransform aff) {
404 switch (aff.getType()) {
405 case AffineTransform.TYPE_IDENTITY:
406 case AffineTransform.TYPE_TRANSLATION:
407 // Radius doesn't change
408 return;
409 default:
410 // Scale the radius as appropriate
411 final double[] fp = new double[]{x, y, x + r, y};
412 aff.transform(fp, 0, fp, 0, 2);
413 r = (float)Math.sqrt(Math.pow(fp[2] - fp[0], 2) + Math.pow(fp[3] - fp[1], 2));
418 static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
419 Tree.exportDTD(sb_header, hs, indent);
420 final String type = "t2_treeline";
421 if (hs.contains(type)) return;
422 hs.add(type);
423 sb_header.append(indent).append("<!ELEMENT t2_treeline (t2_node*,").append(Displayable.commonDTDChildren()).append(")>\n");
424 Displayable.exportDTD(type, sb_header, hs, indent);
427 /** Export the radius only if it is larger than zero. */
428 @Override
429 protected boolean exportXMLNodeAttributes(final StringBuilder indent, final StringBuilder sb, final Node<Float> node) {
430 if (node.getData() > 0) sb.append(" r=\"").append(node.getData()).append('\"');
431 return true;
434 @Override
435 protected boolean exportXMLNodeData(final StringBuilder indent, final StringBuilder sb, final Node<Float> node) {
436 return false;
439 /** Testing for performance, 100 iterations:
440 * A: 3307 (current, with clearing of table on the fly)
441 * B: 4613 (without clearing table)
442 * C: 4012 (without point caching)
444 * Although in short runs (10 iterations) A can get very bad:
445 * (first run of 10)
446 * A: 664
447 * B: 611
448 * C: 196
449 * (second run of 10)
450 * A: 286
451 * B: 314
452 * C: 513 <-- gets worse !?
454 * Differences are not so huge in any case.
457 static final public void testMeshGenerationPerformance(int n_iterations) {
458 // test 3D mesh generation
460 Layer la = Display.getFrontLayer();
461 java.util.Random rnd = new java.util.Random(67779);
462 Node root = new RadiusNode(rnd.nextFloat(), rnd.nextFloat(), la);
463 Node parent = root;
464 for (int i=0; i<10000; i++) {
465 Node child = new RadiusNode(rnd.nextFloat(), rnd.nextFloat(), la);
466 parent.add(child, Node.MAX_EDGE_CONFIDENCE);
467 if (0 == i % 100) {
468 // add a branch of 100 nodes
469 Node pa = parent;
470 for (int k = 0; k<100; k++) {
471 Node ch = new RadiusNode(rnd.nextFloat(), rnd.nextFloat(), la);
472 pa.add(ch, Node.MAX_EDGE_CONFIDENCE);
473 pa = ch;
476 parent = child;
479 final AffineTransform at = new AffineTransform(1, 0, 0, 1, 67, 134);
481 final ArrayList list = new ArrayList();
483 final LinkedList<Node> todo = new LinkedList<Node>();
485 final float scale = 0.345f;
486 final Calibration cal = la.getParent().getCalibration();
487 final float pixelWidthScaled = (float) cal.pixelWidth * scale;
488 final float pixelHeightScaled = (float) cal.pixelHeight * scale;
489 final int sign = cal.pixelDepth < 0 ? -1 : 1;
490 final Map<Node,Point3f> points = new HashMap<Node,Point3f>();
492 // A few performance tests are needed:
493 // 1 - if the map caching of points helps or recomputing every time is cheaper than lookup
494 // 2 - if removing no-longer-needed points from the map helps lookup or overall slows down
496 long t0 = System.currentTimeMillis();
497 for (int i=0; i<n_iterations; i++) {
498 // A -- current method
499 points.clear();
500 todo.clear();
501 todo.add(root);
502 list.clear();
503 final float[] fps = new float[2];
505 boolean go = true;
506 while (go) {
507 final Node node = todo.removeFirst();
508 // Add children to todo list if any
509 if (null != node.children) {
510 for (final Node nd : node.children) todo.add(nd);
512 go = !todo.isEmpty();
513 // Get node's 3D coordinate
514 Point3f p = points.get(node);
515 if (null == p) {
516 fps[0] = node.x;
517 fps[1] = node.y;
518 at.transform(fps, 0, fps, 0, 1);
519 p = new Point3f(fps[0] * pixelWidthScaled,
520 fps[1] * pixelHeightScaled,
521 (float)node.la.getZ() * pixelWidthScaled * sign);
522 points.put(node, p);
524 if (null != node.parent) {
525 // Create a line to the parent
526 list.add(points.get(node.parent));
527 list.add(p);
528 if (go && node.parent != todo.getFirst().parent) {
529 // node.parent point no longer needed (last child just processed)
530 points.remove(node.parent);
535 System.out.println("A: " + (System.currentTimeMillis() - t0));
538 t0 = System.currentTimeMillis();
539 for (int i=0; i<n_iterations; i++) {
541 points.clear();
542 todo.clear();
543 todo.add(root);
544 list.clear();
545 final float[] fps = new float[2];
547 // Simpler method, not clearing no-longer-used nodes from map
548 while (!todo.isEmpty()) {
549 final Node node = todo.removeFirst();
550 // Add children to todo list if any
551 if (null != node.children) {
552 for (final Node nd : node.children) todo.add(nd);
554 // Get node's 3D coordinate
555 Point3f p = points.get(node);
556 if (null == p) {
557 fps[0] = node.x;
558 fps[1] = node.y;
559 at.transform(fps, 0, fps, 0, 1);
560 p = new Point3f(fps[0] * pixelWidthScaled,
561 fps[1] * pixelHeightScaled,
562 (float)node.la.getZ() * pixelWidthScaled * sign);
563 points.put(node, p);
565 if (null != node.parent) {
566 // Create a line to the parent
567 list.add(points.get(node.parent));
568 list.add(p);
572 System.out.println("B: " + (System.currentTimeMillis() - t0));
574 t0 = System.currentTimeMillis();
575 for (int i=0; i<n_iterations; i++) {
577 todo.clear();
578 todo.add(root);
579 list.clear();
581 // Simplest method: no caching in a map
582 final float[] fp = new float[4];
583 while (!todo.isEmpty()) {
584 final Node node = todo.removeFirst();
585 // Add children to todo list if any
586 if (null != node.children) {
587 for (final Node nd : node.children) todo.add(nd);
589 if (null != node.parent) {
590 // Create a line to the parent
591 fp[0] = node.x;
592 fp[1] = node.y;
593 fp[2] = node.parent.x;
594 fp[3] = node.parent.y;
595 at.transform(fp, 0, fp, 0, 2);
596 list.add(new Point3f(fp[2] * pixelWidthScaled,
597 fp[3] * pixelHeightScaled,
598 (float)node.parent.la.getZ() * pixelWidthScaled * sign));
599 list.add(new Point3f(fp[0] * pixelWidthScaled,
600 fp[1] * pixelHeightScaled,
601 (float)node.la.getZ() * pixelWidthScaled * sign));
605 System.out.println("C: " + (System.currentTimeMillis() - t0));
609 /** Returns a list of two lists: the List<Point3f> and the corresponding List<Color3f>. */
610 public MeshData generateMesh(final double scale_, int parallels) {
611 // Construct a mesh made of straight tubes for each edge, and balls of the same ending diameter on the nodes.
613 // TODO:
614 // With some cleverness, such meshes could be welded together by merging the nearest vertices on the ball
615 // surfaces, or by cleaving the surface where the diameter of the tube cuts it.
616 // A tougher problem is where tubes cut each other, but perhaps if the resulting mesh is still non-manifold, it's ok.
618 final float scale = (float)scale_;
619 if (parallels < 3) parallels = 3;
621 // Simple ball-and-stick model
623 // first test: just the nodes as icosahedrons with 1 subdivision
625 final Calibration cal = layer_set.getCalibration();
626 final float pixelWidthScaled = (float)cal.pixelWidth * scale;
627 final float pixelHeightScaled = (float)cal.pixelHeight * scale;
628 final int sign = cal.pixelDepth < 0 ? -1 : 1;
630 final List<Point3f> ico = M.createIcosahedron(1, 1);
631 final List<Point3f> ps = new ArrayList<Point3f>();
633 // A plane made of as many edges as parallels, with radius 1
634 // Perpendicular vector of the plane is 0,0,1
635 final List<Point3f> plane = new ArrayList<Point3f>();
636 final double inc_rads = (Math.PI * 2) / parallels;
637 double angle = 0;
638 for (int i=0; i<parallels; i++) {
639 plane.add(new Point3f((float)Math.cos(angle), (float)Math.sin(angle), 0));
640 angle += inc_rads;
642 final Vector3f vplane = new Vector3f(0, 0, 1);
643 final Transform3D t = new Transform3D();
644 final AxisAngle4f aa = new AxisAngle4f();
646 final List<Color3f> colors = new ArrayList<Color3f>();
647 final Color3f cf = new Color3f(this.color);
648 final HashMap<Color,Color3f> cached_colors = new HashMap<Color,Color3f>();
649 cached_colors.put(this.color, cf);
651 for (final Set<Node<Float>> nodes : node_layer_map.values()) {
652 for (final Node<Float> nd : nodes) {
653 Point2D.Double po = transformPoint(nd.x, nd.y);
654 final float x = (float)po.x * pixelWidthScaled;
655 final float y = (float)po.y * pixelHeightScaled;
656 final float z = (float)nd.la.getZ() * pixelWidthScaled * sign;
657 final float r = ((RadiusNode)nd).r * pixelWidthScaled; // TODO r is not transformed by the AffineTransform
658 for (final Point3f vert : ico) {
659 final Point3f v = new Point3f(vert);
660 v.x = v.x * r + x;
661 v.y = v.y * r + y;
662 v.z = v.z * r + z;
663 ps.add(v);
666 int n_verts = ico.size();
668 // Tube from parent to child
669 // Check if a 3D volume representation is necessary for this segment
670 if (null != nd.parent && (0 != nd.parent.getData() || 0 != nd.getData())) {
672 po = null;
674 // parent:
675 final Point2D.Double pp = transformPoint(nd.parent.x, nd.parent.y);
676 final float parx = (float)pp.x * pixelWidthScaled;
677 final float pary = (float)pp.y * pixelWidthScaled;
678 final float parz = (float)nd.parent.la.getZ() * pixelWidthScaled * sign;
679 final float parr = ((RadiusNode)nd.parent).r * pixelWidthScaled; // TODO r is not transformed by the AffineTransform
681 // the vector perpendicular to the plane is 0,0,1
682 // the vector from parent to child is:
683 final Vector3f vpc = new Vector3f(x - parx, y - pary, z - parz);
685 if (x == parx && y == pary) {
686 aa.set(0, 0, 1, 0);
687 } else {
688 final Vector3f cross = new Vector3f();
689 cross.cross(vpc, vplane);
690 cross.normalize(); // not needed?
691 aa.set(cross.x, cross.y, cross.z, -vplane.angle(vpc));
693 t.set(aa);
696 final List<Point3f> parent_verts = transform(t, plane, parx, pary, parz, parr);
697 final List<Point3f> child_verts = transform(t, plane, x, y, z, r);
699 for (int i=1; i<parallels; i++) {
700 addTriangles(ps, parent_verts, child_verts, i-1, i);
701 n_verts += 6;
703 // faces from last to first:
704 addTriangles(ps, parent_verts, child_verts, parallels -1, 0);
705 n_verts += 6;
708 // Colors for each segment:
709 Color3f c;
710 if (null == nd.color) {
711 c = cf;
712 } else {
713 c = cached_colors.get(nd.color);
714 if (null == c) {
715 c = new Color3f(nd.color);
716 cached_colors.put(nd.color, c);
719 while (n_verts > 0) {
720 n_verts--;
721 colors.add(c);
726 //Utils.log2("Treeline MeshData lists of same length: " + (ps.size() == colors.size()));
728 return new MeshData(ps, colors);
731 static private final void addTriangles(final List<Point3f> ps, final List<Point3f> parent_verts, final List<Point3f> child_verts, final int i0, final int i1) {
732 // one triangle
733 ps.add(new Point3f(parent_verts.get(i0)));
734 ps.add(new Point3f(parent_verts.get(i1)));
735 ps.add(new Point3f(child_verts.get(i0)));
736 // another
737 ps.add(new Point3f(parent_verts.get(i1)));
738 ps.add(new Point3f(child_verts.get(i1)));
739 ps.add(new Point3f(child_verts.get(i0)));
742 static private final List<Point3f> transform(final Transform3D t, final List<Point3f> plane, final float x, final float y, final float z, final float radius) {
743 final List<Point3f> ps = new ArrayList<Point3f>(plane.size());
744 for (final Point3f p2 : plane) {
745 final Point3f p = new Point3f(p2);
746 p.scale(radius);
747 t.transform(p);
748 p.x += x;
749 p.y += y;
750 p.z += z;
751 ps.add(p);
753 return ps;
756 @Override
757 public void keyPressed(final KeyEvent ke) {
758 if (isTagging()) {
759 super.keyPressed(ke);
760 return;
762 final int tool = ProjectToolbar.getToolId();
763 try {
764 if (ProjectToolbar.PEN == tool) {
765 final Object origin = ke.getSource();
766 if (! (origin instanceof DisplayCanvas)) {
767 ke.consume();
768 return;
770 final DisplayCanvas dc = (DisplayCanvas)origin;
771 final Layer layer = dc.getDisplay().getLayer();
772 final Point p = dc.getCursorLoc(); // as offscreen coords
774 switch (ke.getKeyCode()) {
775 case KeyEvent.VK_O:
776 if (askAdjustRadius(p.x, p.y, layer, dc.getMagnification())) {
777 ke.consume();
779 break;
782 } finally {
783 if (!ke.isConsumed()) {
784 super.keyPressed(ke);
789 private boolean askAdjustRadius(final float x, final float y, final Layer layer, final double magnification) {
790 final Collection<Node<Float>> nodes = node_layer_map.get(layer);
791 if (null == nodes) return false;
793 RadiusNode nd = (RadiusNode) findClosestNodeW(nodes, x, y, magnification);
794 if (null == nd) {
795 final Node<Float> last = getLastVisited();
796 if (last.getLayer() == layer) nd = (RadiusNode)last;
798 if (null == nd) return false;
800 return askAdjustRadius(nd);
803 protected boolean askAdjustRadius(final Node<Float> nd) {
805 final GenericDialog gd = new GenericDialog("Adjust radius");
806 final Calibration cal = layer_set.getCalibration();
807 String unit = cal.getUnit();
808 if (!unit.toLowerCase().startsWith("pixel")) {
809 final String[] units = new String[]{"pixels", unit};
810 gd.addChoice("Units:", units, units[1]);
811 gd.addNumericField("Radius:", nd.getData() * cal.pixelWidth, 2);
812 final TextField tfr = (TextField) gd.getNumericFields().get(0);
813 ((Choice)gd.getChoices().get(0)).addItemListener(new ItemListener() {
814 @Override
815 public void itemStateChanged(final ItemEvent ie) {
816 final double val = Double.parseDouble(tfr.getText());
817 if (Double.isNaN(val)) return;
818 tfr.setText(Double.toString(units[0] == ie.getItem() ?
819 val / cal.pixelWidth
820 : val * cal.pixelWidth));
823 } else {
824 unit = null;
825 gd.addNumericField("Radius:", nd.getData(), 2, 10, "pixels");
827 final String[] choices = {"this node only", "nodes until next branch or end node", "entire subtree"};
828 gd.addChoice("Apply to:", choices, choices[0]);
829 gd.showDialog();
830 if (gd.wasCanceled()) return false;
831 double radius = gd.getNextNumber();
832 if (Double.isNaN(radius) || radius < 0) {
833 Utils.log("Invalid radius: " + radius);
834 return false;
836 if (null != unit && 1 == gd.getNextChoiceIndex() && 0 != radius) {
837 // convert radius from units to pixels
838 radius = radius / cal.pixelWidth;
840 final float r = (float)radius;
841 final Node.Operation<Float> op = new Node.Operation<Float>() {
842 @Override
843 public void apply(final Node<Float> node) throws Exception {
844 node.setData(r);
847 // Apply to:
848 try {
849 layer_set.addDataEditStep(this);
850 switch (gd.getNextChoiceIndex()) {
851 case 0:
852 // Just the node
853 nd.setData(r);
854 break;
855 case 1:
856 // All the way to the next branch or end point
857 nd.applyToSlab(op);
858 break;
859 case 2:
860 // To the entire subtree of nodes
861 nd.applyToSubtree(op);
862 break;
863 default:
864 return false;
866 layer_set.addDataEditStep(this);
867 } catch (final Exception e) {
868 IJError.print(e);
869 layer_set.undoOneStep();
872 calculateBoundingBox(layer);
873 Display.repaint(layer_set);
875 return true;
878 @Override
879 protected Rectangle getBounds(final Collection<? extends Node<Float>> nodes) {
880 Rectangle box = null;
881 for (final RadiusNode nd : (Collection<RadiusNode>) nodes) {
882 if (null == nd.parent) {
883 if (null == box) box = new Rectangle((int)nd.x, (int)nd.y, 1, 1);
884 else box.add((int)nd.x, (int)nd.y);
885 continue;
887 // Get the segment with the parent node
888 if (null == box) box = nd.getSegment().getBounds();
889 else box.add(nd.getSegment().getBounds());
891 return box;
894 private class RadiusMeasurementPair extends Tree<Float>.MeasurementPair
896 public RadiusMeasurementPair(final Tree<Float>.NodePath np) {
897 super(np);
899 /** A list of calibrated radii, one per node in the path.*/
900 @Override
901 protected List<Float> calibratedData() {
902 final ArrayList<Float> data = new ArrayList<Float>();
903 final AffineTransform aff = new AffineTransform(Treeline.this.at);
904 final Calibration cal = layer_set.getCalibration();
905 aff.preConcatenate(new AffineTransform(cal.pixelWidth, 0, 0, cal.pixelHeight, 0, 0));
906 final float[] fp = new float[4];
907 for (final Node<Float> nd : super.path) {
908 final Float r = nd.getData();
909 if (null == r) data.add(null);
910 fp[0] = nd.x;
911 fp[1] = nd.y;
912 fp[2] = nd.x + r.floatValue();
913 fp[3] = nd.y;
914 aff.transform(fp, 0, fp, 0, 2);
915 data.add((float)Math.sqrt(Math.pow(fp[2] - fp[0], 2) + Math.pow(fp[3] - fp[1], 2)));
917 return data;
919 @Override
920 public String getResultsTableTitle() {
921 return "Treeline tagged pairs";
923 @Override
924 public ResultsTable toResultsTable(ResultsTable rt, final int index, final double scale, final int resample) {
925 if (null == rt) {
926 final String unit = layer_set.getCalibration().getUnit();
927 rt = Utils.createResultsTable(getResultsTableTitle(),
928 new String[]{"id", "index", "length " + unit, "volume " + unit + "^3",
929 "shortest diameter " + unit, "longest diameter " + unit,
930 "average diameter " + unit, "stdDev diameter"});
932 rt.incrementCounter();
933 rt.addValue(0, Treeline.this.id);
934 rt.addValue(1, index);
935 rt.addValue(2, distance);
936 double minRadius = Double.MAX_VALUE,
937 maxRadius = 0,
938 sumRadii = 0,
939 volume = 0;
940 int i = 0;
941 double last_r = 0;
942 Point3f last_p = null;
943 final Iterator<Point3f> itp = coords.iterator();
944 final Iterator<Float> itr = data.iterator();
945 while (itp.hasNext()) {
946 final double r = itr.next();
947 final Point3f p = itp.next();
949 minRadius = Math.min(minRadius, r);
950 maxRadius = Math.max(maxRadius, r);
951 sumRadii += r;
953 if (i > 0) {
954 volume += M.volumeOfTruncatedCone(r, last_r, p.distance(last_p));
957 i += 1;
958 last_r = r;
959 last_p = p;
961 final int count = path.size();
962 final double avgRadius = (sumRadii / count);
963 // Compute standard deviation of the diameters:
964 double s = 0;
965 for (final Float r : data) s += Math.pow(2 * (r - avgRadius), 2);
966 final double stdDev = Math.sqrt(s / count);
968 rt.addValue(3, volume);
969 rt.addValue(4, minRadius * 2);
970 rt.addValue(5, maxRadius * 2);
971 rt.addValue(6, avgRadius * 2);
972 rt.addValue(7, stdDev);
973 return rt;
975 @Override
976 public MeshData createMesh(final double scale, final int parallels) {
977 final Treeline sub = new Treeline(project, -1, title, width, height, alpha, visible, color, locked, new AffineTransform(Treeline.this.at));
978 sub.layer_set = Treeline.this.layer_set;
979 sub.root = path.get(0);
980 sub.cacheSubtree(path);
981 return sub.generateMesh(scale, parallels);
985 @Override
986 protected MeasurementPair createMeasurementPair(final NodePath np) {
987 return new RadiusMeasurementPair(np);