Use internal SNAPSHOT couplings again
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / Treeline.java
blob50aed29405e7260ce12890addb6a5e110071e7df
1 package ini.trakem2.display;
3 import ij.gui.GenericDialog;
4 import ij.measure.Calibration;
5 import ij.measure.ResultsTable;
6 import ini.trakem2.Project;
7 import ini.trakem2.utils.IJError;
8 import ini.trakem2.utils.M;
9 import ini.trakem2.utils.ProjectToolbar;
10 import ini.trakem2.utils.Utils;
12 import java.awt.AlphaComposite;
13 import java.awt.Choice;
14 import java.awt.Color;
15 import java.awt.Composite;
16 import java.awt.Graphics2D;
17 import java.awt.Point;
18 import java.awt.Polygon;
19 import java.awt.Rectangle;
20 import java.awt.Shape;
21 import java.awt.TextField;
22 import java.awt.event.ItemEvent;
23 import java.awt.event.ItemListener;
24 import java.awt.event.KeyEvent;
25 import java.awt.event.MouseEvent;
26 import java.awt.event.MouseWheelEvent;
27 import java.awt.geom.AffineTransform;
28 import java.awt.geom.Area;
29 import java.awt.geom.Ellipse2D;
30 import java.awt.geom.Point2D;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Set;
39 import javax.media.j3d.Transform3D;
40 import javax.vecmath.AxisAngle4f;
41 import javax.vecmath.Color3f;
42 import javax.vecmath.Point3f;
43 import javax.vecmath.Vector3f;
45 public class Treeline extends Tree<Float> {
47 static protected float last_radius = -1;
49 public Treeline(Project project, 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(float lx, float ly, Layer la, Node<?> modelNode) {
71 return new RadiusNode(lx, ly, la, null == modelNode ? 0 : ((RadiusNode)modelNode).r);
74 @Override
75 public Node<Float> newNode(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 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(MouseEvent me, Layer la, int x_p, int y_p, 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 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(MouseEvent me, Layer la, int x_p, int y_p, int x_d, int y_d, int x_d_old, 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 Node<Float> nd = getActive();
147 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(MouseEvent me, Layer la, int x_p, int y_p, int x_d, int y_d, int x_r, 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(MouseWheelEvent mwe) {
170 final int modifiers = mwe.getModifiers();
171 if (0 == ( (MouseWheelEvent.SHIFT_MASK | MouseWheelEvent.ALT_MASK) ^ modifiers)) {
172 Object source = mwe.getSource();
173 if (! (source instanceof DisplayCanvas)) return;
174 DisplayCanvas dc = (DisplayCanvas)source;
175 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 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(float inc, float x, float y, Layer layer, DisplayCanvas dc) {
193 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 public Node<Float> newInstance(final float lx, final float ly, final Layer layer) {
220 return new RadiusNode(lx, ly, layer, 0);
223 /** Set the radius to a positive value. When zero or negative, it's set to zero. */
224 public final boolean setData(final Float radius) {
225 this.r = radius > 0 ? radius : 0;
226 return true;
228 public final Float getData() { return this.r; }
230 public final Float getDataCopy() { return this.r; }
232 @Override
233 public boolean isRoughlyInside(final Rectangle localbox) {
234 if (0 == this.r) {
235 if (null == parent) {
236 return localbox.contains((int)this.x, (int)this.y);
237 } else {
238 if (0 == parent.getData()) { // parent.getData() == ((RadiusNode)parent).r
239 return localbox.intersectsLine(parent.x, parent.y, this.x, this.y);
240 } else {
241 return segmentIntersects(localbox);
244 } else {
245 if (null == parent) {
246 return localbox.contains((int)this.x, (int)this.y);
247 } else {
248 return segmentIntersects(localbox);
253 private final Polygon getSegment() {
254 final RadiusNode parent = (RadiusNode) this.parent;
255 float vx = parent.x - this.x;
256 float vy = parent.y - this.y;
257 float len = (float) Math.sqrt(vx*vx + vy*vy);
258 if (0 == len) {
259 // Points are on top of each other
260 return new Polygon(new int[]{(int)this.x, (int)Math.ceil(parent.x)},
261 new int[]{(int)this.y, (int)Math.ceil(parent.y)}, 2);
263 vx /= len;
264 vy /= len;
265 // perpendicular vector
266 final float vx90 = -vy;
267 final float vy90 = vx;
268 final float vx270 = vy;
269 final float vy270 = -vx;
271 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)},
272 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)},
276 // The human compiler at work!
277 /** Detect intersection between localRect and the bounds of getSegment() */
278 private final boolean segmentIntersects(final Rectangle localRect) {
279 final RadiusNode parent = (RadiusNode) this.parent;
280 float vx = parent.x - this.x;
281 float vy = parent.y - this.y;
282 final float len = (float) Math.sqrt(vx*vx + vy*vy);
283 if (0 == len) {
284 // Points are on top of each other
285 return localRect.contains(this.x, this.y);
287 vx /= len;
288 vy /= len;
289 // perpendicular vector
290 //final float vx90 = -vy;
291 //final float vy90 = vx;
292 //final float vx270 = vy;
293 //final float vy270 = -vx;
295 final float x1 = parent.x + (-vy) /*vx90*/ * parent.r,
296 y1 = parent.y + vx /*vy90*/ * parent.r,
297 x2 = parent.x + vy /*vx270*/ * parent.r,
298 y2 = parent.y + (-vx) /*vy270*/ * parent.r,
299 x3 = this.x + vy /*vx270*/ * this.r,
300 y3 = this.y + (-vx) /*vy270*/ * this.r,
301 x4 = this.x + (-vy) /*vx90*/ * this.r,
302 y4 = this.y + vx /*vy90*/ * this.r;
303 final float min_x = Math.min(Math.min(x1, x2), Math.min(x3, x4)),
304 min_y = Math.min(Math.min(y1, y2), Math.min(y3, y4)),
305 max_x = Math.max(Math.max(x1, x2), Math.max(x3, x4)),
306 max_y = Math.max(Math.max(y1, y2), Math.max(y3, y4));
308 final float w = max_x - min_x,
309 h = max_y - min_y;
311 return min_x + w > localRect.x
312 && min_y + h > localRect.y
313 && min_x < localRect.x + localRect.width
314 && min_y < localRect.y + localRect.height;
317 // As above, but inline:
318 return min_x + max_x - min_x > localRect.x
319 && min_y + max_y - min_y > localRect.y
320 && min_x < localRect.x + localRect.width
321 && min_y < localRect.y + localRect.height;
323 // May give false negatives!
324 //return localRect.contains((int)(parent.x + vx90 * parent.r), (int)(parent.y + vy90 * parent.r))
325 // || localRect.contains((int)(parent.x + vx270 * parent.r), (int)(parent.y + vy270 * parent.r))
326 // || localRect.contains((int)(this.x + vx270 * this.r), (int)(this.y + vy270 * this.r))
327 // || localRect.contains((int)(this.x + vx90 * this.r), (int)(this.y + vy90 * this.r));
330 @Override
331 public void paintData(final Graphics2D g, final Rectangle srcRect,
332 final Tree<Float> tree, final AffineTransform to_screen, final Color cc,
333 final Layer active_layer) {
334 if (null == this.parent) return; // doing it here for less total cost
335 if (0 == this.r && 0 == parent.getData()) return;
337 // Two transformations, but it's only 4 points each and it's necessary
338 //final Polygon segment = getSegment();
339 //if (!tree.at.createTransformedShape(segment).intersects(srcRect)) return Node.FALSE;
340 //final Shape shape = to_screen.createTransformedShape(segment);
341 final Shape shape = to_screen.createTransformedShape(getSegment());
342 final Composite c = g.getComposite();
343 final float alpha = tree.getAlpha();
344 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha > 0.4f ? 0.4f : alpha));
345 g.setColor(cc);
346 g.fill(shape);
347 g.setComposite(c);
348 g.draw(shape); // in Tree's composite mode (such as an alpha)
351 /** Expects @param a in local coords. */
352 @Override
353 public boolean intersects(final Area a) {
354 if (0 == r) return a.contains(x, y);
355 return M.intersects(a, new Area(new Ellipse2D.Float(x-r, y-r, r+r, r+r)));
356 // TODO: not the getSegment() ?
359 @Override
360 public void apply(final mpicbg.models.CoordinateTransform ct, final Area roi) {
361 // store the point
362 float ox = x,
363 oy = y;
364 // transform the point itself
365 super.apply(ct, roi);
366 // transform the radius: assume it's a point to its right
367 if (0 != r) {
368 float[] fp = new float[]{ox + r, oy};
369 ct.applyInPlace(fp);
370 r = Math.abs(fp[0] - this.x);
373 @Override
374 public void apply(final VectorDataTransform vdt) {
375 for (final VectorDataTransform.ROITransform rt : vdt.transforms) {
376 // Apply only the first one that contains the point
377 if (rt.roi.contains(x, y)) {
378 // Store point
379 float ox = x,
380 oy = y;
381 // Transform point
382 float[] fp = new float[]{x, y};
383 rt.ct.applyInPlace(fp);
384 x = fp[0];
385 y = fp[1];
386 // Transform the radius: assume it's a point to the right of the untransformed point
387 if (0 != r) {
388 fp[0] = ox + r;
389 fp[1] = oy;
390 rt.ct.applyInPlace(fp);
391 r = Math.abs(fp[0] - this.x);
393 break;
398 @Override
399 protected void transformData(final AffineTransform aff) {
400 switch (aff.getType()) {
401 case AffineTransform.TYPE_IDENTITY:
402 case AffineTransform.TYPE_TRANSLATION:
403 // Radius doesn't change
404 return;
405 default:
406 // Scale the radius as appropriate
407 final float[] fp = new float[]{x, y, x + r, y};
408 aff.transform(fp, 0, fp, 0, 2);
409 r = (float)Math.sqrt(Math.pow(fp[2] - fp[0], 2) + Math.pow(fp[3] - fp[1], 2));
414 static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
415 Tree.exportDTD(sb_header, hs, indent);
416 final String type = "t2_treeline";
417 if (hs.contains(type)) return;
418 hs.add(type);
419 sb_header.append(indent).append("<!ELEMENT t2_treeline (t2_node*,").append(Displayable.commonDTDChildren()).append(")>\n");
420 Displayable.exportDTD(type, sb_header, hs, indent);
423 /** Export the radius only if it is larger than zero. */
424 @Override
425 protected boolean exportXMLNodeAttributes(final StringBuilder indent, final StringBuilder sb, final Node<Float> node) {
426 if (node.getData() > 0) sb.append(" r=\"").append(node.getData()).append('\"');
427 return true;
430 @Override
431 protected boolean exportXMLNodeData(final StringBuilder indent, final StringBuilder sb, final Node<Float> node) {
432 return false;
435 /** Testing for performance, 100 iterations:
436 * A: 3307 (current, with clearing of table on the fly)
437 * B: 4613 (without clearing table)
438 * C: 4012 (without point caching)
440 * Although in short runs (10 iterations) A can get very bad:
441 * (first run of 10)
442 * A: 664
443 * B: 611
444 * C: 196
445 * (second run of 10)
446 * A: 286
447 * B: 314
448 * C: 513 <-- gets worse !?
450 * Differences are not so huge in any case.
453 static final public void testMeshGenerationPerformance(int n_iterations) {
454 // test 3D mesh generation
456 Layer la = Display.getFrontLayer();
457 java.util.Random rnd = new java.util.Random(67779);
458 Node root = new RadiusNode(rnd.nextFloat(), rnd.nextFloat(), la);
459 Node parent = root;
460 for (int i=0; i<10000; i++) {
461 Node child = new RadiusNode(rnd.nextFloat(), rnd.nextFloat(), la);
462 parent.add(child, Node.MAX_EDGE_CONFIDENCE);
463 if (0 == i % 100) {
464 // add a branch of 100 nodes
465 Node pa = parent;
466 for (int k = 0; k<100; k++) {
467 Node ch = new RadiusNode(rnd.nextFloat(), rnd.nextFloat(), la);
468 pa.add(ch, Node.MAX_EDGE_CONFIDENCE);
469 pa = ch;
472 parent = child;
475 final AffineTransform at = new AffineTransform(1, 0, 0, 1, 67, 134);
477 final ArrayList list = new ArrayList();
479 final LinkedList<Node> todo = new LinkedList<Node>();
481 final float scale = 0.345f;
482 final Calibration cal = la.getParent().getCalibration();
483 final float pixelWidthScaled = (float) cal.pixelWidth * scale;
484 final float pixelHeightScaled = (float) cal.pixelHeight * scale;
485 final int sign = cal.pixelDepth < 0 ? -1 : 1;
486 final Map<Node,Point3f> points = new HashMap<Node,Point3f>();
488 // A few performance tests are needed:
489 // 1 - if the map caching of points helps or recomputing every time is cheaper than lookup
490 // 2 - if removing no-longer-needed points from the map helps lookup or overall slows down
492 long t0 = System.currentTimeMillis();
493 for (int i=0; i<n_iterations; i++) {
494 // A -- current method
495 points.clear();
496 todo.clear();
497 todo.add(root);
498 list.clear();
499 final float[] fps = new float[2];
501 boolean go = true;
502 while (go) {
503 final Node node = todo.removeFirst();
504 // Add children to todo list if any
505 if (null != node.children) {
506 for (final Node nd : node.children) todo.add(nd);
508 go = !todo.isEmpty();
509 // Get node's 3D coordinate
510 Point3f p = points.get(node);
511 if (null == p) {
512 fps[0] = node.x;
513 fps[1] = node.y;
514 at.transform(fps, 0, fps, 0, 1);
515 p = new Point3f(fps[0] * pixelWidthScaled,
516 fps[1] * pixelHeightScaled,
517 (float)node.la.getZ() * pixelWidthScaled * sign);
518 points.put(node, p);
520 if (null != node.parent) {
521 // Create a line to the parent
522 list.add(points.get(node.parent));
523 list.add(p);
524 if (go && node.parent != todo.getFirst().parent) {
525 // node.parent point no longer needed (last child just processed)
526 points.remove(node.parent);
531 System.out.println("A: " + (System.currentTimeMillis() - t0));
534 t0 = System.currentTimeMillis();
535 for (int i=0; i<n_iterations; i++) {
537 points.clear();
538 todo.clear();
539 todo.add(root);
540 list.clear();
541 final float[] fps = new float[2];
543 // Simpler method, not clearing no-longer-used nodes from map
544 while (!todo.isEmpty()) {
545 final Node node = todo.removeFirst();
546 // Add children to todo list if any
547 if (null != node.children) {
548 for (final Node nd : node.children) todo.add(nd);
550 // Get node's 3D coordinate
551 Point3f p = points.get(node);
552 if (null == p) {
553 fps[0] = node.x;
554 fps[1] = node.y;
555 at.transform(fps, 0, fps, 0, 1);
556 p = new Point3f(fps[0] * pixelWidthScaled,
557 fps[1] * pixelHeightScaled,
558 (float)node.la.getZ() * pixelWidthScaled * sign);
559 points.put(node, p);
561 if (null != node.parent) {
562 // Create a line to the parent
563 list.add(points.get(node.parent));
564 list.add(p);
568 System.out.println("B: " + (System.currentTimeMillis() - t0));
570 t0 = System.currentTimeMillis();
571 for (int i=0; i<n_iterations; i++) {
573 todo.clear();
574 todo.add(root);
575 list.clear();
577 // Simplest method: no caching in a map
578 final float[] fp = new float[4];
579 while (!todo.isEmpty()) {
580 final Node node = todo.removeFirst();
581 // Add children to todo list if any
582 if (null != node.children) {
583 for (final Node nd : node.children) todo.add(nd);
585 if (null != node.parent) {
586 // Create a line to the parent
587 fp[0] = node.x;
588 fp[1] = node.y;
589 fp[2] = node.parent.x;
590 fp[3] = node.parent.y;
591 at.transform(fp, 0, fp, 0, 2);
592 list.add(new Point3f(fp[2] * pixelWidthScaled,
593 fp[3] * pixelHeightScaled,
594 (float)node.parent.la.getZ() * pixelWidthScaled * sign));
595 list.add(new Point3f(fp[0] * pixelWidthScaled,
596 fp[1] * pixelHeightScaled,
597 (float)node.la.getZ() * pixelWidthScaled * sign));
601 System.out.println("C: " + (System.currentTimeMillis() - t0));
605 /** Returns a list of two lists: the List<Point3f> and the corresponding List<Color3f>. */
606 public MeshData generateMesh(double scale_, int parallels) {
607 // Construct a mesh made of straight tubes for each edge, and balls of the same ending diameter on the nodes.
609 // TODO:
610 // With some cleverness, such meshes could be welded together by merging the nearest vertices on the ball
611 // surfaces, or by cleaving the surface where the diameter of the tube cuts it.
612 // A tougher problem is where tubes cut each other, but perhaps if the resulting mesh is still non-manifold, it's ok.
614 final float scale = (float)scale_;
615 if (parallels < 3) parallels = 3;
617 // Simple ball-and-stick model
619 // first test: just the nodes as icosahedrons with 1 subdivision
621 final Calibration cal = layer_set.getCalibration();
622 final float pixelWidthScaled = (float)cal.pixelWidth * scale;
623 final float pixelHeightScaled = (float)cal.pixelHeight * scale;
624 final int sign = cal.pixelDepth < 0 ? -1 : 1;
626 final List<Point3f> ico = M.createIcosahedron(1, 1);
627 final List<Point3f> ps = new ArrayList<Point3f>();
629 // A plane made of as many edges as parallels, with radius 1
630 // Perpendicular vector of the plane is 0,0,1
631 final List<Point3f> plane = new ArrayList<Point3f>();
632 final double inc_rads = (Math.PI * 2) / parallels;
633 double angle = 0;
634 for (int i=0; i<parallels; i++) {
635 plane.add(new Point3f((float)Math.cos(angle), (float)Math.sin(angle), 0));
636 angle += inc_rads;
638 final Vector3f vplane = new Vector3f(0, 0, 1);
639 final Transform3D t = new Transform3D();
640 final AxisAngle4f aa = new AxisAngle4f();
642 final List<Color3f> colors = new ArrayList<Color3f>();
643 final Color3f cf = new Color3f(this.color);
644 final HashMap<Color,Color3f> cached_colors = new HashMap<Color,Color3f>();
645 cached_colors.put(this.color, cf);
647 for (final Set<Node<Float>> nodes : node_layer_map.values()) {
648 for (final Node<Float> nd : nodes) {
649 Point2D.Double po = transformPoint(nd.x, nd.y);
650 final float x = (float)po.x * pixelWidthScaled;
651 final float y = (float)po.y * pixelHeightScaled;
652 final float z = (float)nd.la.getZ() * pixelWidthScaled * sign;
653 final float r = ((RadiusNode)nd).r * pixelWidthScaled; // TODO r is not transformed by the AffineTransform
654 for (final Point3f vert : ico) {
655 Point3f v = new Point3f(vert);
656 v.x = v.x * r + x;
657 v.y = v.y * r + y;
658 v.z = v.z * r + z;
659 ps.add(v);
662 int n_verts = ico.size();
664 // Tube from parent to child
665 // Check if a 3D volume representation is necessary for this segment
666 if (null != nd.parent && (0 != nd.parent.getData() || 0 != nd.getData())) {
668 po = null;
670 // parent:
671 Point2D.Double pp = transformPoint(nd.parent.x, nd.parent.y);
672 final float parx = (float)pp.x * pixelWidthScaled;
673 final float pary = (float)pp.y * pixelWidthScaled;
674 final float parz = (float)nd.parent.la.getZ() * pixelWidthScaled * sign;
675 final float parr = ((RadiusNode)nd.parent).r * pixelWidthScaled; // TODO r is not transformed by the AffineTransform
677 // the vector perpendicular to the plane is 0,0,1
678 // the vector from parent to child is:
679 Vector3f vpc = new Vector3f(x - parx, y - pary, z - parz);
681 if (x == parx && y == pary) {
682 aa.set(0, 0, 1, 0);
683 } else {
684 Vector3f cross = new Vector3f();
685 cross.cross(vpc, vplane);
686 cross.normalize(); // not needed?
687 aa.set(cross.x, cross.y, cross.z, -vplane.angle(vpc));
689 t.set(aa);
692 final List<Point3f> parent_verts = transform(t, plane, parx, pary, parz, parr);
693 final List<Point3f> child_verts = transform(t, plane, x, y, z, r);
695 for (int i=1; i<parallels; i++) {
696 addTriangles(ps, parent_verts, child_verts, i-1, i);
697 n_verts += 6;
699 // faces from last to first:
700 addTriangles(ps, parent_verts, child_verts, parallels -1, 0);
701 n_verts += 6;
704 // Colors for each segment:
705 Color3f c;
706 if (null == nd.color) {
707 c = cf;
708 } else {
709 c = cached_colors.get(nd.color);
710 if (null == c) {
711 c = new Color3f(nd.color);
712 cached_colors.put(nd.color, c);
715 while (n_verts > 0) {
716 n_verts--;
717 colors.add(c);
722 //Utils.log2("Treeline MeshData lists of same length: " + (ps.size() == colors.size()));
724 return new MeshData(ps, colors);
727 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) {
728 // one triangle
729 ps.add(new Point3f(parent_verts.get(i0)));
730 ps.add(new Point3f(parent_verts.get(i1)));
731 ps.add(new Point3f(child_verts.get(i0)));
732 // another
733 ps.add(new Point3f(parent_verts.get(i1)));
734 ps.add(new Point3f(child_verts.get(i1)));
735 ps.add(new Point3f(child_verts.get(i0)));
738 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) {
739 final List<Point3f> ps = new ArrayList<Point3f>(plane.size());
740 for (final Point3f p2 : plane) {
741 final Point3f p = new Point3f(p2);
742 p.scale(radius);
743 t.transform(p);
744 p.x += x;
745 p.y += y;
746 p.z += z;
747 ps.add(p);
749 return ps;
752 @Override
753 public void keyPressed(KeyEvent ke) {
754 if (isTagging()) {
755 super.keyPressed(ke);
756 return;
758 final int tool = ProjectToolbar.getToolId();
759 try {
760 if (ProjectToolbar.PEN == tool) {
761 Object origin = ke.getSource();
762 if (! (origin instanceof DisplayCanvas)) {
763 ke.consume();
764 return;
766 DisplayCanvas dc = (DisplayCanvas)origin;
767 Layer layer = dc.getDisplay().getLayer();
768 final Point p = dc.getCursorLoc(); // as offscreen coords
770 switch (ke.getKeyCode()) {
771 case KeyEvent.VK_O:
772 if (askAdjustRadius(p.x, p.y, layer, dc.getMagnification())) {
773 ke.consume();
775 break;
778 } finally {
779 if (!ke.isConsumed()) {
780 super.keyPressed(ke);
785 private boolean askAdjustRadius(final float x, final float y, final Layer layer, final double magnification) {
786 final Collection<Node<Float>> nodes = node_layer_map.get(layer);
787 if (null == nodes) return false;
789 RadiusNode nd = (RadiusNode) findClosestNodeW(nodes, x, y, magnification);
790 if (null == nd) {
791 Node<Float> last = getLastVisited();
792 if (last.getLayer() == layer) nd = (RadiusNode)last;
794 if (null == nd) return false;
796 return askAdjustRadius(nd);
799 protected boolean askAdjustRadius(final Node<Float> nd) {
801 GenericDialog gd = new GenericDialog("Adjust radius");
802 final Calibration cal = layer_set.getCalibration();
803 String unit = cal.getUnit();
804 if (!unit.toLowerCase().startsWith("pixel")) {
805 final String[] units = new String[]{"pixels", unit};
806 gd.addChoice("Units:", units, units[1]);
807 gd.addNumericField("Radius:", nd.getData() * cal.pixelWidth, 2);
808 final TextField tfr = (TextField) gd.getNumericFields().get(0);
809 ((Choice)gd.getChoices().get(0)).addItemListener(new ItemListener() {
810 public void itemStateChanged(ItemEvent ie) {
811 final double val = Double.parseDouble(tfr.getText());
812 if (Double.isNaN(val)) return;
813 tfr.setText(Double.toString(units[0] == ie.getItem() ?
814 val / cal.pixelWidth
815 : val * cal.pixelWidth));
818 } else {
819 unit = null;
820 gd.addNumericField("Radius:", nd.getData(), 2, 10, "pixels");
822 final String[] choices = {"this node only", "nodes until next branch or end node", "entire subtree"};
823 gd.addChoice("Apply to:", choices, choices[0]);
824 gd.showDialog();
825 if (gd.wasCanceled()) return false;
826 double radius = gd.getNextNumber();
827 if (Double.isNaN(radius) || radius < 0) {
828 Utils.log("Invalid radius: " + radius);
829 return false;
831 if (null != unit && 1 == gd.getNextChoiceIndex() && 0 != radius) {
832 // convert radius from units to pixels
833 radius = radius / cal.pixelWidth;
835 final float r = (float)radius;
836 final Node.Operation<Float> op = new Node.Operation<Float>() {
837 @Override
838 public void apply(Node<Float> node) throws Exception {
839 node.setData(r);
842 // Apply to:
843 try {
844 layer_set.addDataEditStep(this);
845 switch (gd.getNextChoiceIndex()) {
846 case 0:
847 // Just the node
848 nd.setData(r);
849 break;
850 case 1:
851 // All the way to the next branch or end point
852 nd.applyToSlab(op);
853 break;
854 case 2:
855 // To the entire subtree of nodes
856 nd.applyToSubtree(op);
857 break;
858 default:
859 return false;
861 layer_set.addDataEditStep(this);
862 } catch (Exception e) {
863 IJError.print(e);
864 layer_set.undoOneStep();
867 calculateBoundingBox(layer);
868 Display.repaint(layer_set);
870 return true;
873 @Override
874 protected Rectangle getBounds(final Collection<? extends Node<Float>> nodes) {
875 Rectangle box = null;
876 for (final RadiusNode nd : (Collection<RadiusNode>) nodes) {
877 if (null == nd.parent) {
878 if (null == box) box = new Rectangle((int)nd.x, (int)nd.y, 1, 1);
879 else box.add((int)nd.x, (int)nd.y);
880 continue;
882 // Get the segment with the parent node
883 if (null == box) box = nd.getSegment().getBounds();
884 else box.add(nd.getSegment().getBounds());
886 return box;
889 private class RadiusMeasurementPair extends Tree<Float>.MeasurementPair
891 public RadiusMeasurementPair(Tree<Float>.NodePath np) {
892 super(np);
894 /** A list of calibrated radii, one per node in the path.*/
895 @Override
896 protected List<Float> calibratedData() {
897 final ArrayList<Float> data = new ArrayList<Float>();
898 final AffineTransform aff = new AffineTransform(Treeline.this.at);
899 final Calibration cal = layer_set.getCalibration();
900 aff.preConcatenate(new AffineTransform(cal.pixelWidth, 0, 0, cal.pixelHeight, 0, 0));
901 final float[] fp = new float[4];
902 for (final Node<Float> nd : super.path) {
903 Float r = nd.getData();
904 if (null == r) data.add(null);
905 fp[0] = nd.x;
906 fp[1] = nd.y;
907 fp[2] = nd.x + r.floatValue();
908 fp[3] = nd.y;
909 aff.transform(fp, 0, fp, 0, 2);
910 data.add((float)Math.sqrt(Math.pow(fp[2] - fp[0], 2) + Math.pow(fp[3] - fp[1], 2)));
912 return data;
914 @Override
915 public String getResultsTableTitle() {
916 return "Treeline tagged pairs";
918 @Override
919 public ResultsTable toResultsTable(ResultsTable rt, int index, double scale, int resample) {
920 if (null == rt) {
921 final String unit = layer_set.getCalibration().getUnit();
922 rt = Utils.createResultsTable(getResultsTableTitle(),
923 new String[]{"id", "index", "length " + unit, "volume " + unit + "^3",
924 "shortest diameter " + unit, "longest diameter " + unit,
925 "average diameter " + unit, "stdDev diameter"});
927 rt.incrementCounter();
928 rt.addValue(0, Treeline.this.id);
929 rt.addValue(1, index);
930 rt.addValue(2, distance);
931 double minRadius = Double.MAX_VALUE,
932 maxRadius = 0,
933 sumRadii = 0,
934 volume = 0;
935 int i = 0;
936 double last_r = 0;
937 Point3f last_p = null;
938 final Iterator<Point3f> itp = coords.iterator();
939 final Iterator<Float> itr = data.iterator();
940 while (itp.hasNext()) {
941 final double r = itr.next();
942 final Point3f p = itp.next();
944 minRadius = Math.min(minRadius, r);
945 maxRadius = Math.max(maxRadius, r);
946 sumRadii += r;
948 if (i > 0) {
949 volume += M.volumeOfTruncatedCone(r, last_r, p.distance(last_p));
952 i += 1;
953 last_r = r;
954 last_p = p;
956 final int count = path.size();
957 final double avgRadius = (sumRadii / count);
958 // Compute standard deviation of the diameters:
959 double s = 0;
960 for (final Float r : data) s += Math.pow(2 * (r - avgRadius), 2);
961 final double stdDev = Math.sqrt(s / count);
963 rt.addValue(3, volume);
964 rt.addValue(4, minRadius * 2);
965 rt.addValue(5, maxRadius * 2);
966 rt.addValue(6, avgRadius * 2);
967 rt.addValue(7, stdDev);
968 return rt;
970 @Override
971 public MeshData createMesh(final double scale, final int parallels) {
972 Treeline sub = new Treeline(project, -1, title, width, height, alpha, visible, color, locked, new AffineTransform(Treeline.this.at));
973 sub.layer_set = Treeline.this.layer_set;
974 sub.root = path.get(0);
975 sub.cacheSubtree(path);
976 return sub.generateMesh(scale, parallels);
980 @Override
981 protected MeasurementPair createMeasurementPair(NodePath np) {
982 return new RadiusMeasurementPair(np);