1 package ini
.trakem2
.display
;
3 import java
.awt
.AlphaComposite
;
4 import java
.awt
.Choice
;
6 import java
.awt
.Composite
;
7 import java
.awt
.Graphics2D
;
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
;
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
);
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
);
65 public Tree
<Float
> newInstance() {
66 return new Treeline(project
, project
.getLoader().getNextId(), title
, width
, height
, alpha
, visible
, color
, locked
, at
);
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
);
75 public Node
<Float
> newNode(final HashMap
<String
,String
> ht_attr
) {
76 return new RadiusNode(ht_attr
);
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());
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());
100 Utils
.log("Can't adjust radius: found more than 1 node within visible area!");
103 // So: only one node within visible area of the canvas:
104 // Adjust the radius by shift+alt+drag
108 if (!this.at
.isIdentity()) {
109 final Point2D
.Double po
= inverseTransformPoint(x_p
, y_p
);
115 nd
.setData((float)Math
.sqrt(Math
.pow(xp
- nd
.x
, 2) + Math
.pow(yp
- nd
.y
, 2)));
122 super.mousePressed(me
, la
, x_p
, y_p
, mag
);
125 protected boolean requireAltDownToEditRadius() {
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
);
137 if (me
.isShiftDown() && !Utils
.isControlDown(me
)) {
138 // transform to the local coordinates
141 if (!this.at
.isIdentity()) {
142 final Point2D
.Double po
= inverseTransformPoint(x_d
, y_d
);
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));
154 super.mouseDragged(me
, la
, x_p
, y_p
, x_d
, y_d
, x_d_old
, y_d_old
);
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());
165 super.mouseReleased(me
, la
, x_p
, y_p
, x_d
, y_d
, x_r
, y_r
);
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);
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!");
198 nearest
.setData(nearest
.getData() + inc
);
202 static public class RadiusNode
extends Node
<Float
> {
205 public RadiusNode(final float lx
, final float ly
, final Layer la
) {
208 public RadiusNode(final float lx
, final float ly
, final Layer la
, final float radius
) {
212 /** To reconstruct from XML, without a layer. */
213 public RadiusNode(final HashMap
<String
,String
> attr
) {
215 final String sr
= (String
)attr
.get("r");
216 this.r
= null == sr ?
0 : Float
.parseFloat(sr
);
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. */
226 public final boolean setData(final Float radius
) {
227 this.r
= radius
> 0 ? radius
: 0;
231 public final Float
getData() { return this.r
; }
234 public final Float
getDataCopy() { return this.r
; }
237 public boolean isRoughlyInside(final Rectangle localbox
) {
239 if (null == parent
) {
240 return localbox
.contains((int)this.x
, (int)this.y
);
242 if (0 == parent
.getData()) { // parent.getData() == ((RadiusNode)parent).r
243 return localbox
.intersectsLine(parent
.x
, parent
.y
, this.x
, this.y
);
245 return segmentIntersects(localbox
);
249 if (null == parent
) {
250 return localbox
.contains((int)this.x
, (int)this.y
);
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
);
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);
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
);
288 // Points are on top of each other
289 return localRect
.contains(this.x
, this.y
);
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,
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));
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
));
352 g
.draw(shape
); // in Tree's composite mode (such as an alpha)
355 /** Expects @param a in local coords. */
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() ?
364 public void apply(final mpicbg
.models
.CoordinateTransform ct
, final Area roi
) {
368 // transform the point itself
369 super.apply(ct
, roi
);
370 // transform the radius: assume it's a point to its right
372 final double[] fp
= new double[]{ox
+ r
, oy
};
374 r
= ( float )Math
.abs(fp
[0] - this.x
);
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
)) {
386 final double[] fp
= new double[]{x
, y
};
387 rt
.ct
.applyInPlace(fp
);
390 // Transform the radius: assume it's a point to the right of the untransformed point
394 rt
.ct
.applyInPlace(fp
);
395 r
= ( float )Math
.abs(fp
[0] - this.x
);
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
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;
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. */
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('\"');
435 protected boolean exportXMLNodeData(final StringBuilder indent
, final StringBuilder sb
, final Node
<Float
> node
) {
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:
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);
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);
468 // add a branch of 100 nodes
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);
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
503 final float[] fps = new float[2];
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);
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);
524 if (null != node.parent) {
525 // Create a line to the parent
526 list.add(points.get(node.parent));
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++) {
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);
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);
565 if (null != node.parent) {
566 // Create a line to the parent
567 list.add(points.get(node.parent));
572 System.out.println("B: " + (System.currentTimeMillis() - t0));
574 t0 = System.currentTimeMillis();
575 for (int i=0; i<n_iterations; i++) {
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
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.
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
;
638 for (int i
=0; i
<parallels
; i
++) {
639 plane
.add(new Point3f((float)Math
.cos(angle
), (float)Math
.sin(angle
), 0));
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
);
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())) {
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
) {
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
));
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
);
703 // faces from last to first:
704 addTriangles(ps
, parent_verts
, child_verts
, parallels
-1, 0);
708 // Colors for each segment:
710 if (null == nd
.color
) {
713 c
= cached_colors
.get(nd
.color
);
715 c
= new Color3f(nd
.color
);
716 cached_colors
.put(nd
.color
, c
);
719 while (n_verts
> 0) {
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
) {
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
)));
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
);
757 public void keyPressed(final KeyEvent ke
) {
759 super.keyPressed(ke
);
762 final int tool
= ProjectToolbar
.getToolId();
764 if (ProjectToolbar
.PEN
== tool
) {
765 final Object origin
= ke
.getSource();
766 if (! (origin
instanceof DisplayCanvas
)) {
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()) {
776 if (askAdjustRadius(p
.x
, p
.y
, layer
, dc
.getMagnification())) {
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
);
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() {
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() ?
820 : val
* cal
.pixelWidth
));
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]);
830 if (gd
.wasCanceled()) return false;
831 double radius
= gd
.getNextNumber();
832 if (Double
.isNaN(radius
) || radius
< 0) {
833 Utils
.log("Invalid radius: " + radius
);
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
>() {
843 public void apply(final Node
<Float
> node
) throws Exception
{
849 layer_set
.addDataEditStep(this);
850 switch (gd
.getNextChoiceIndex()) {
856 // All the way to the next branch or end point
860 // To the entire subtree of nodes
861 nd
.applyToSubtree(op
);
866 layer_set
.addDataEditStep(this);
867 } catch (final Exception e
) {
869 layer_set
.undoOneStep();
872 calculateBoundingBox(layer
);
873 Display
.repaint(layer_set
);
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
);
887 // Get the segment with the parent node
888 if (null == box
) box
= nd
.getSegment().getBounds();
889 else box
.add(nd
.getSegment().getBounds());
894 private class RadiusMeasurementPair
extends Tree
<Float
>.MeasurementPair
896 public RadiusMeasurementPair(final Tree
<Float
>.NodePath np
) {
899 /** A list of calibrated radii, one per node in the path.*/
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);
912 fp
[2] = nd
.x
+ r
.floatValue();
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)));
920 public String
getResultsTableTitle() {
921 return "Treeline tagged pairs";
924 public ResultsTable
toResultsTable(ResultsTable rt
, final int index
, final double scale
, final int resample
) {
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
,
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
);
954 volume
+= M
.volumeOfTruncatedCone(r
, last_r
, p
.distance(last_p
));
961 final int count
= path
.size();
962 final double avgRadius
= (sumRadii
/ count
);
963 // Compute standard deviation of the diameters:
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
);
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
);
986 protected MeasurementPair
createMeasurementPair(final NodePath np
) {
987 return new RadiusMeasurementPair(np
);