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
;
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
);
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(float lx
, float ly
, Layer la
, Node
<?
> modelNode
) {
71 return new RadiusNode(lx
, ly
, la
, null == modelNode ?
0 : ((RadiusNode
)modelNode
).r
);
75 public Node
<Float
> newNode(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 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(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());
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(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
);
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 Node
<Float
> nd
= getActive();
147 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(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());
165 super.mouseReleased(me
, la
, x_p
, y_p
, x_d
, y_d
, x_r
, y_r
);
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);
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!");
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
);
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;
228 public final Float
getData() { return this.r
; }
230 public final Float
getDataCopy() { return this.r
; }
233 public boolean isRoughlyInside(final Rectangle localbox
) {
235 if (null == parent
) {
236 return localbox
.contains((int)this.x
, (int)this.y
);
238 if (0 == parent
.getData()) { // parent.getData() == ((RadiusNode)parent).r
239 return localbox
.intersectsLine(parent
.x
, parent
.y
, this.x
, this.y
);
241 return segmentIntersects(localbox
);
245 if (null == parent
) {
246 return localbox
.contains((int)this.x
, (int)this.y
);
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
);
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);
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
);
284 // Points are on top of each other
285 return localRect
.contains(this.x
, this.y
);
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,
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));
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
));
348 g
.draw(shape
); // in Tree's composite mode (such as an alpha)
351 /** Expects @param a in local coords. */
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() ?
360 public void apply(final mpicbg
.models
.CoordinateTransform ct
, final Area roi
) {
364 // transform the point itself
365 super.apply(ct
, roi
);
366 // transform the radius: assume it's a point to its right
368 float[] fp
= new float[]{ox
+ r
, oy
};
370 r
= Math
.abs(fp
[0] - this.x
);
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
)) {
382 float[] fp
= new float[]{x
, y
};
383 rt
.ct
.applyInPlace(fp
);
386 // Transform the radius: assume it's a point to the right of the untransformed point
390 rt
.ct
.applyInPlace(fp
);
391 r
= Math
.abs(fp
[0] - this.x
);
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
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;
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. */
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('\"');
431 protected boolean exportXMLNodeData(final StringBuilder indent
, final StringBuilder sb
, final Node
<Float
> node
) {
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:
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);
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);
464 // add a branch of 100 nodes
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);
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
499 final float[] fps = new float[2];
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);
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);
520 if (null != node.parent) {
521 // Create a line to the parent
522 list.add(points.get(node.parent));
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++) {
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);
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);
561 if (null != node.parent) {
562 // Create a line to the parent
563 list.add(points.get(node.parent));
568 System.out.println("B: " + (System.currentTimeMillis() - t0));
570 t0 = System.currentTimeMillis();
571 for (int i=0; i<n_iterations; i++) {
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
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.
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
;
634 for (int i
=0; i
<parallels
; i
++) {
635 plane
.add(new Point3f((float)Math
.cos(angle
), (float)Math
.sin(angle
), 0));
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
);
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())) {
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
) {
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
));
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
);
699 // faces from last to first:
700 addTriangles(ps
, parent_verts
, child_verts
, parallels
-1, 0);
704 // Colors for each segment:
706 if (null == nd
.color
) {
709 c
= cached_colors
.get(nd
.color
);
711 c
= new Color3f(nd
.color
);
712 cached_colors
.put(nd
.color
, c
);
715 while (n_verts
> 0) {
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
) {
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
)));
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
);
753 public void keyPressed(KeyEvent ke
) {
755 super.keyPressed(ke
);
758 final int tool
= ProjectToolbar
.getToolId();
760 if (ProjectToolbar
.PEN
== tool
) {
761 Object origin
= ke
.getSource();
762 if (! (origin
instanceof DisplayCanvas
)) {
766 DisplayCanvas dc
= (DisplayCanvas
)origin
;
767 Layer layer
= dc
.getDisplay().getLayer();
768 final Point p
= dc
.getCursorLoc(); // as offscreen coords
770 switch (ke
.getKeyCode()) {
772 if (askAdjustRadius(p
.x
, p
.y
, layer
, dc
.getMagnification())) {
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
);
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() ?
815 : val
* cal
.pixelWidth
));
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]);
825 if (gd
.wasCanceled()) return false;
826 double radius
= gd
.getNextNumber();
827 if (Double
.isNaN(radius
) || radius
< 0) {
828 Utils
.log("Invalid radius: " + radius
);
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
>() {
838 public void apply(Node
<Float
> node
) throws Exception
{
844 layer_set
.addDataEditStep(this);
845 switch (gd
.getNextChoiceIndex()) {
851 // All the way to the next branch or end point
855 // To the entire subtree of nodes
856 nd
.applyToSubtree(op
);
861 layer_set
.addDataEditStep(this);
862 } catch (Exception e
) {
864 layer_set
.undoOneStep();
867 calculateBoundingBox(layer
);
868 Display
.repaint(layer_set
);
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
);
882 // Get the segment with the parent node
883 if (null == box
) box
= nd
.getSegment().getBounds();
884 else box
.add(nd
.getSegment().getBounds());
889 private class RadiusMeasurementPair
extends Tree
<Float
>.MeasurementPair
891 public RadiusMeasurementPair(Tree
<Float
>.NodePath np
) {
894 /** A list of calibrated radii, one per node in the path.*/
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);
907 fp
[2] = nd
.x
+ r
.floatValue();
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)));
915 public String
getResultsTableTitle() {
916 return "Treeline tagged pairs";
919 public ResultsTable
toResultsTable(ResultsTable rt
, int index
, double scale
, int resample
) {
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
,
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
);
949 volume
+= M
.volumeOfTruncatedCone(r
, last_r
, p
.distance(last_p
));
956 final int count
= path
.size();
957 final double avgRadius
= (sumRadii
/ count
);
958 // Compute standard deviation of the diameters:
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
);
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
);
981 protected MeasurementPair
createMeasurementPair(NodePath np
) {
982 return new RadiusMeasurementPair(np
);