3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2008 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
23 package ini
.trakem2
.display
;
26 import ij
.gui
.Toolbar
;
28 import ij
.measure
.Calibration
;
29 import ij
.measure
.ResultsTable
;
31 import ini
.trakem2
.imaging
.LayerStack
;
32 import ini
.trakem2
.Project
;
33 import ini
.trakem2
.utils
.Bureaucrat
;
34 import ini
.trakem2
.utils
.IJError
;
35 import ini
.trakem2
.utils
.M
;
36 import ini
.trakem2
.utils
.ProjectToolbar
;
37 import ini
.trakem2
.utils
.Utils
;
38 import ini
.trakem2
.utils
.Worker
;
39 import ini
.trakem2
.vector
.VectorString3D
;
41 import java
.awt
.AlphaComposite
;
42 import java
.awt
.Color
;
43 import java
.awt
.Composite
;
44 import java
.awt
.event
.KeyEvent
;
45 import java
.awt
.event
.MouseEvent
;
46 import java
.awt
.geom
.AffineTransform
;
47 import java
.awt
.geom
.Area
;
48 import java
.awt
.geom
.Point2D
;
49 import java
.awt
.Graphics2D
;
50 import java
.awt
.Polygon
;
51 import java
.awt
.Rectangle
;
52 import java
.awt
.Shape
;
53 import java
.util
.ArrayList
;
54 import java
.util
.HashMap
;
55 import java
.util
.HashSet
;
56 import java
.util
.Iterator
;
57 import java
.util
.List
;
59 import java
.util
.Random
;
61 import features
.ComputeCurvatures
;
63 import tracing
.SearchThread
;
64 import tracing
.TracerThread
;
65 import tracing
.SearchProgressCallback
;
67 import javax
.vecmath
.Vector3d
;
70 /** A sequence of points that make multiple chained line segments. */
71 public class Polyline
extends ZDisplayable
implements Line3D
{
73 /**The number of points.*/
74 protected int n_points
;
75 /**The array of clicked x,y points as [2][n].*/
76 protected double[][] p
= new double[2][0];
77 /**The array of Layers over which the points of this pipe live */
78 protected long[] p_layer
= new long[0];
80 /** New empty Polyline. */
81 public Polyline(Project project
, String title
) {
82 super(project
, title
, 0, 0);
87 public Polyline(Project project
, long id
, String title
, double width
, double height
, float alpha
, boolean visible
, Color color
, boolean locked
, AffineTransform at
) {
88 super(project
, id
, title
, locked
, at
, width
, height
);
89 this.visible
= visible
;
91 this.visible
= visible
;
93 this.n_points
= -1; //used as a flag to signal "I have points, but unloaded"
96 /** Reconstruct from XML. */
97 public Polyline(final Project project
, final long id
, final HashMap ht_attr
, final HashMap ht_links
) {
98 super(project
, id
, ht_attr
, ht_links
);
99 // parse specific data
100 for (Iterator it
= ht_attr
.entrySet().iterator(); it
.hasNext(); ) {
101 Map
.Entry entry
= (Map
.Entry
)it
.next();
102 String key
= (String
)entry
.getKey();
103 String data
= (String
)entry
.getValue();
104 if (key
.equals("d")) {
106 // parse the SVG points data
107 ArrayList al_p
= new ArrayList();
110 // sequence is: M p[0][0],p[1][0] L p[0][1],p[1][1] L p[0][2],p[1][2] ...
112 int i_start
= data
.indexOf('M');
113 int i_L
= data
.indexOf('L', i_start
+1);
116 if (p
[0].length
== next
) enlargeArrays();
119 int i_comma
= data
.indexOf(',', i_start
+1);
120 p
[0][next
] = Double
.parseDouble(data
.substring(i_start
+1, i_comma
));
122 i_L
= data
.indexOf('L', i_comma
);
124 if (-1 == i_L
) i_end
= data
.length();
125 p
[1][next
] = Double
.parseDouble(data
.substring(i_comma
+1, i_end
));
127 // prepare next point
132 // scale arrays back, so minimal size and also same size as p_layer
133 p
= new double[][]{Utils
.copy(p
[0], n_points
), Utils
.copy(p
[1], n_points
)};
134 } else if (key
.equals("layer_ids")) {
135 // parse comma-separated list of layer ids. Creates empty Layer instances with the proper id, that will be replaced later.
136 final String
[] layer_ids
= data
.replaceAll(" ", "").trim().split(",");
137 this.p_layer
= new long[layer_ids
.length
];
138 for (int i
=0; i
<layer_ids
.length
; i
++) {
139 if (i
== p_layer
.length
) enlargeArrays();
140 this.p_layer
[i
] = Long
.parseLong(layer_ids
[i
]);
146 /**Increase the size of the arrays by 5.*/
147 synchronized protected void enlargeArrays() {
150 synchronized protected void enlargeArrays(int n_more
) {
152 int length
= p
[0].length
;
154 double[][] p_copy
= new double[2][length
+ n_more
];
155 long[] p_layer_copy
= new long[length
+ n_more
];
157 System
.arraycopy(p
[0], 0, p_copy
[0], 0, length
);
158 System
.arraycopy(p
[1], 0, p_copy
[1], 0, length
);
159 System
.arraycopy(p_layer
, 0, p_layer_copy
, 0, length
);
162 this.p_layer
= p_layer_copy
;
165 /**Find a point in an array, with a precision dependent on the magnification. Only points in the given layer are considered, the rest are ignored. Returns -1 if none found. */
166 synchronized protected int findPoint(final int x_p
, final int y_p
, final long layer_id
, final double mag
) {
168 double d
= (10.0D
/ mag
);
170 double min_dist
= Double
.MAX_VALUE
;
171 for (int i
=0; i
<n_points
; i
++) {
172 double dist
= Math
.abs(x_p
- p
[0][i
]) + Math
.abs(y_p
- p
[1][i
]);
173 if (layer_id
== p_layer
[i
] && dist
<= d
&& dist
<= min_dist
) {
181 /**Remove a point from the bezier backbone and its two associated control points.*/
182 synchronized protected void removePoint(final int index
) {
183 // check preconditions:
186 } else if (n_points
- 1 == index
) {
190 //one point out (but not the last)
193 // shift all points after 'index' one position to the left:
194 for (int i
=index
; i
<n_points
; i
++) {
195 p
[0][i
] = p
[0][i
+1]; //the +1 doesn't fail ever because the n_points has been adjusted above, but the arrays are still the same size. The case of deleting the last point is taken care above.
197 p_layer
[i
] = p_layer
[i
+1];
201 // Reset or fix autotracing records
202 if (index
< last_autotrace_start
&& n_points
> 0) {
203 last_autotrace_start
--;
204 } else last_autotrace_start
= -1;
207 updateInDatabase("points");
210 /**Move backbone point by the given deltas.*/
211 public void dragPoint(final int index
, final int dx
, final int dy
) {
212 if (index
< 0 || index
>= n_points
) return;
216 // Reset autotracing records
217 if (-1 != last_autotrace_start
&& index
>= last_autotrace_start
) {
218 last_autotrace_start
= -1;
222 /**Add a point either at the end or between two existing points, with accuracy depending on magnification. The width of the new point is that of the closest point after which it is inserted.*/
223 synchronized protected int addPoint(int x_p
, int y_p
, long layer_id
, double magnification
) {
224 if (-1 == n_points
) setupForDisplay(); //reload
225 //lookup closest point and then get the closest clicked point to it
226 int index
= findPoint(x_p
, y_p
, layer_id
, magnification
);
228 if (p
[0].length
== n_points
) {
232 if (0 == n_points
|| 1 == n_points
|| index
+ 1 == n_points
) {
234 p
[0][n_points
] = x_p
;
235 p
[1][n_points
] = y_p
;
236 p_layer
[n_points
] = layer_id
;
239 last_autotrace_start
= -1;
240 } else if (-1 == index
) {
241 // decide whether to append at the end or prepend at the beginning
242 // compute distance in the 3D space to the first and last points
243 final Calibration cal
= layer_set
.getCalibration();
244 final double lz
= layer_set
.getLayer(layer_id
).getZ();
245 final double p0z
=layer_set
.getLayer(p_layer
[0]).getZ();
246 final double pNz
=layer_set
.getLayer(p_layer
[n_points
-1]).getZ();
247 double sqdist0
= (p
[0][0] - x_p
) * (p
[0][0] - x_p
) * cal
.pixelWidth
* cal
.pixelWidth
248 + (p
[1][0] - y_p
) * (p
[1][0] - y_p
) * cal
.pixelHeight
* cal
.pixelHeight
249 + (lz
- p0z
) * (lz
- p0z
) * cal
.pixelWidth
* cal
.pixelWidth
; // double multiplication by pixelWidth, ok, since it's what it's used to compute the pixel position in Z
250 double sqdistN
= (p
[0][n_points
-1] - x_p
) * (p
[0][n_points
-1] - x_p
) * cal
.pixelWidth
* cal
.pixelWidth
251 + (p
[1][n_points
-1] - y_p
) * (p
[1][n_points
-1] - y_p
) * cal
.pixelHeight
* cal
.pixelHeight
252 + (lz
- pNz
) * (lz
- pNz
) * cal
.pixelWidth
* cal
.pixelWidth
;
253 if (sqdistN
< sqdist0
) {
255 p
[0][n_points
] = x_p
;
256 p
[1][n_points
] = y_p
;
257 p_layer
[n_points
] = layer_id
;
260 last_autotrace_start
= -1;
262 // prepend at the beginning
263 for (int i
=n_points
-1; i
>-1; i
--) {
266 p_layer
[i
+1] = p_layer
[i
];
270 p_layer
[0] = layer_id
;
273 if (-1 != last_autotrace_start
) last_autotrace_start
++;
277 index
++; //so it is added after the closest point;
278 // 1 - copy second half of array
279 int sh_length
= n_points
-index
;
280 double[][] p_copy
= new double[2][sh_length
];
281 long[] p_layer_copy
= new long[sh_length
];
282 System
.arraycopy(p
[0], index
, p_copy
[0], 0, sh_length
);
283 System
.arraycopy(p
[1], index
, p_copy
[1], 0, sh_length
);
284 System
.arraycopy(p_layer
, index
, p_layer_copy
, 0, sh_length
);
285 // 2 - insert value into 'p' (the two control arrays get the same value)
288 p_layer
[index
] = layer_id
;
289 // 3 - copy second half into the array
290 System
.arraycopy(p_copy
[0], 0, p
[0], index
+1, sh_length
);
291 System
.arraycopy(p_copy
[1], 0, p
[1], index
+1, sh_length
);
292 System
.arraycopy(p_layer_copy
, 0, p_layer
, index
+1, sh_length
);
294 // Reset autotracing records
295 if (index
< last_autotrace_start
) {
296 last_autotrace_start
++;
305 synchronized protected void appendPoints(final double[] px
, final double[] py
, final long[] p_layer_ids
, int len
) {
306 for (int i
=0, next
=n_points
; i
<len
; i
++, next
++) {
307 if (next
== p
[0].length
) enlargeArrays();
310 p_layer
[next
] = p_layer_ids
[i
];
313 updateInDatabase("points");
316 public void paint(final Graphics2D g
, final double magnification
, final boolean active
, final int channels
, final Layer active_layer
) {
317 if (0 == n_points
) return;
318 if (-1 == n_points
) {
319 // load points from the database
322 //arrange transparency
323 Composite original_composite
= null;
325 original_composite
= g
.getComposite();
326 g
.setComposite(AlphaComposite
.getInstance(AlphaComposite
.SRC_OVER
, alpha
));
329 // local pointers, since they may be transformed
330 int n_points
= this.n_points
;
331 double[][] p
= this.p
;
332 if (!this.at
.isIdentity()) {
333 final Object
[] ob
= getTransformedData();
334 p
= (double[][])ob
[0];
335 n_points
= p
[0].length
;
338 final boolean no_color_cues
= "true".equals(project
.getProperty("no_color_cues"));
339 final long layer_id
= active_layer
.getId();
340 final double z_current
= active_layer
.getZ();
342 // Different approach: a point paints itself and towards the next, except the last point.
344 double z
= layer_set
.getLayer(p_layer
[0]).getZ();
345 boolean paint
= true;
347 if (no_color_cues
) paint
= false;
348 else g
.setColor(Color
.red
);
349 } else if (z
== z_current
) g
.setColor(this.color
);
350 else if (no_color_cues
) paint
= false;
351 else g
.setColor(Color
.blue
);
354 if (paint
&& n_points
> 1) {
355 g
.drawLine((int)p
[0][0], (int)p
[1][0],
356 (int)((p
[0][0] + p
[0][1])/2), (int)((p
[1][0] + p
[1][1])/2));
358 // Paint handle if active and in the current layer
359 if (active
&& layer_id
== p_layer
[0]) {
360 g
.setColor(this.color
);
361 DisplayCanvas
.drawHandle(g
, (int)p
[0][0], (int)p
[1][0], magnification
);
364 for (int i
=1; i
<n_points
; i
++) {
366 z
= layer_set
.getLayer(p_layer
[i
]).getZ();
368 if (no_color_cues
) paint
= false;
369 else g
.setColor(Color
.red
);
370 } else if (z
== z_current
) g
.setColor(this.color
);
371 else if (no_color_cues
) paint
= false;
372 else g
.setColor(Color
.blue
);
373 if (!paint
) continue;
374 // paint half line towards previous point:
375 g
.drawLine((int)p
[0][i
], (int)p
[1][i
],
376 (int)((p
[0][i
] + p
[0][i
-1])/2), (int)((p
[1][i
] + p
[1][i
-1])/2));
377 // paint half line towards next point:
378 if (i
< n_points
-1) {
379 g
.drawLine((int)p
[0][i
], (int)p
[1][i
],
380 (int)((p
[0][i
] + p
[0][i
+1])/2), (int)((p
[1][i
] + p
[1][i
+1])/2));
382 // Paint handle if active and in the current layer
383 if (active
&& layer_id
== p_layer
[i
]) {
384 g
.setColor(this.color
);
385 DisplayCanvas
.drawHandle(g
, (int)p
[0][i
], (int)p
[1][i
], magnification
);
389 //Transparency: fix alpha composite back to original.
390 if (null != original_composite
) {
391 g
.setComposite(original_composite
);
395 public void keyPressed(KeyEvent ke
) {
396 int keyCode
= ke
.getKeyCode();
399 if (-1 == last_autotrace_start
) {
400 if (0 > n_points
) Utils
.log("Cannot remove last set of autotraced points:\n Manual editions exist, or never autotraced.");
404 final int len
= n_points
- last_autotrace_start
;
405 n_points
= last_autotrace_start
;
406 last_autotrace_start
= -1;
408 Utils
.log("Removed " + len
+ " autotraced points.");
410 case KeyEvent
.VK_R
: // reset tracing
411 tr_map
.remove(layer_set
);
413 Utils
.log("Reset tracing data for Polyline " + this);
418 /**Helper vars for mouse events. It's safe to have them static since only one Pipe will be edited at a time.*/
419 static private int index
;
420 static private boolean is_new_point
= false;
422 final static private HashMap
<LayerSet
,TraceParameters
> tr_map
= new HashMap
<LayerSet
,TraceParameters
>();
423 private int last_autotrace_start
= -1;
425 static public void flushTraceCache(final Project project
) {
426 synchronized (tr_map
) {
427 for (Iterator
<LayerSet
> it
= tr_map
.keySet().iterator(); it
.hasNext(); ) {
428 if (it
.next().getProject() == project
) it
.remove();
433 /** Shared between all Polyline of the same LayerSet. The issue of locking doesn't arise because there is only one source of mouse input. If you try to run it programatically with synthetic MouseEvent, that's your problem. */
434 static private class TraceParameters
{
435 boolean update
= true;
436 ImagePlus virtual
= null;
438 ComputeCurvatures hessian
= null;
439 TracerThread tracer
= null; // catched thread for KeyEvent to attempt to stop it
442 public void mousePressed(MouseEvent me
, int x_p
, int y_p
, double mag
) {
443 // transform the x_p, y_p to the local coordinates
446 if (!this.at
.isIdentity()) {
447 final Point2D
.Double po
= inverseTransformPoint(x_p
, y_p
);
452 final int tool
= ProjectToolbar
.getToolId();
454 final Display display
= ((DisplayCanvas
)me
.getSource()).getDisplay();
455 final long layer_id
= display
.getLayer().getId();
458 if (Utils
.isControlDown(me
) && me
.isShiftDown()) {
459 index
= Displayable
.findNearestPoint(p
, n_points
, x_p
, y_p
);
461 index
= findPoint(x_p
, y_p
, layer_id
, mag
);
464 if (ProjectToolbar
.PENCIL
== tool
&& n_points
> 0 && -1 == index
&& !me
.isShiftDown() && !Utils
.isControlDown(me
)) {
465 // Use Mark Longair's tracing: from the clicked point to the last one
466 final double scale
= layer_set
.getVirtualizationScale();
467 // Ok now with all found images, create a virtual stack that provides access to them all, with caching.
468 final Worker
[] worker
= new Worker
[2];
470 TraceParameters tr_
= tr_map
.get(layer_set
);
471 final TraceParameters tr
= null == tr_ ?
new TraceParameters() : tr_
;
473 synchronized (tr_map
) {
474 tr_map
.put(layer_set
, tr
);
479 worker
[0] = new Worker("Preparing Hessian...") { public void run() {
482 Utils
.log("Push ESCAPE key to cancel autotrace anytime.");
483 ImagePlus virtual
= new LayerStack(layer_set
, scale
, ImagePlus
.GRAY8
, Patch
.class, display
.getDisplayChannelAlphas()).getImagePlus();
485 Calibration cal
= virtual
.getCalibration();
486 double minimumSeparation
= 1;
487 if (cal
!= null) minimumSeparation
= Math
.min(cal
.pixelWidth
,
488 Math
.min(cal
.pixelHeight
,
490 ComputeCurvatures hessian
= new ComputeCurvatures(virtual
, minimumSeparation
, null, cal
!= null);
493 tr
.virtual
= virtual
;
495 tr
.hessian
= hessian
;
498 } catch (Exception e
) {
503 Bureaucrat
.createAndStart(worker
[0], project
);
506 Point2D
.Double po
= transformPoint(p
[0][n_points
-1], p
[1][n_points
-1]);
507 final int start_x
= (int)po
.x
;
508 final int start_y
= (int)po
.y
;
509 final int start_z
= layer_set
.indexOf(layer_set
.getLayer(p_layer
[n_points
-1])); // 0-based
510 final int goal_x
= (int)(x_pd
* scale
); // must transform into virtual space
511 final int goal_y
= (int)(y_pd
* scale
);
512 final int goal_z
= layer_set
.indexOf(display
.getLayer());
515 Utils.log2("x_pd, y_pd : " + x_pd + ", " + y_pd);
516 Utils.log2("scale: " + scale);
517 Utils.log2("start: " + start_x + "," + start_y + ", " + start_z);
518 Utils.log2("goal: " + goal_x + "," + goal_y + ", " + goal_z);
519 Utils.log2("virtual: " + tr.virtual);
523 final boolean simplify
= me
.isAltDown();
525 worker
[1] = new Worker("Tracer - waiting on hessian") { public void run() {
528 if (null != worker
[0]) {
529 // Wait until hessian is ready
532 setTaskName("Tracing path");
533 final int reportEveryMilliseconds
= 2000;
534 tr
.tracer
= new TracerThread(tr
.virtual
, 0, 255,
535 120, // timeout seconds
536 reportEveryMilliseconds
,
537 start_x
, start_y
, start_z
,
538 goal_x
, goal_y
, goal_z
,
539 true, // reciproal pix values at start and goal
540 tr
.virtual
.getStackSize() == 1,
542 null == tr
.hessian ?
1 : 4,
545 tr
.tracer
.addProgressListener(new SearchProgressCallback() {
546 public void pointsInSearch(SearchThread source
, int inOpen
, int inClosed
) {
547 worker
[1].setTaskName("Tracing path: open=" + inOpen
+ " closed=" + inClosed
);
549 public void finished(SearchThread source
, boolean success
) {
551 Utils
.logAll("Could NOT trace a path");
554 public void threadStatus(SearchThread source
, int currentStatus
) {
555 // This method gets called every reportEveryMilliseconds
556 if (worker
[1].hasQuitted()) {
557 source
.requestStop();
564 final Path result
= tr
.tracer
.getResult();
568 if (null == result
) {
569 Utils
.log("Finding a path failed"); //: "+
570 // not public //SearchThread.exitReasonStrings[tracer.getExitReason()]);
574 // TODO: precise_x_positions etc are likely to be broken (calibrated or something)
577 // Remove bogus points: those at the end with 0,0 coords
578 int len
= result
.points
;
579 final double[][] pos
= result
.getXYZUnscaled();
580 for (int i
=len
-1; i
>-1; i
--) {
581 if (0 == pos
[0][i
] && 0 == pos
[1][i
]) {
585 // Transform points: undo scale, and bring to this Polyline AffineTransform:
586 final AffineTransform aff
= new AffineTransform();
588 /* 2 */ aff
.concatenate(Polyline
.this.at
.createInverse());
589 /* 1 */ aff
.scale(1/scale
, 1/scale
);
590 final double[] po
= new double[len
* 2];
591 for (int i
=0, j
=0; i
<len
; i
++, j
+=2) {
595 final double[] po2
= new double[len
* 2];
596 aff
.transform(po
, 0, po2
, 0, len
); // what a stupid format: consecutive x,y pairs
598 long[] p_layer_ids
= new long[len
];
599 double[] pox
= new double[len
];
600 double[] poy
= new double[len
];
601 for (int i
=0, j
=0; i
<len
; i
++, j
+=2) {
602 p_layer_ids
[i
] = layer_set
.getNearestLayer(pos
[2][i
]).getId(); // z_positions in 0-(N-1), not in 0-N like slices!
607 // Simplify path: to steps of 5 calibration units, or 5 pixels when not calibrated.
609 Object
[] ob
= Polyline
.simplify(pox
, poy
, p_layer_ids
, 10000, layer_set
);
610 pox
= (double[])ob
[0];
611 poy
= (double[])ob
[1];
612 p_layer_ids
= (long[])ob
[2];
616 // Record the first newly-added autotraced point index:
617 last_autotrace_start
= Polyline
.this.n_points
;
618 Polyline
.this.appendPoints(pox
, poy
, p_layer_ids
, len
);
620 Polyline
.this.repaint(true);
621 Utils
.logAll("Added " + len
+ " new points.");
623 } catch (Exception e
) { IJError
.print(e
); }
626 Bureaucrat
.createAndStart(worker
[1], project
);
633 if (ProjectToolbar
.PEN
== tool
|| ProjectToolbar
.PENCIL
== tool
) {
636 if (Utils
.isControlDown(me
) && me
.isShiftDown() && p_layer
[index
] == Display
.getFrontLayer(this.project
).getId()) {
645 if (-1 != index
&& layer_id
!= p_layer
[index
]) index
= -1; // disable!
647 //if no conditions are met, attempt to add point
648 else if (-1 == index
&& !me
.isShiftDown() && !me
.isAltDown()) {
650 index
= addPoint(x_p
, y_p
, layer_id
, mag
);
658 public void mouseDragged(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_d_old
, int y_d_old
) {
659 // transform to the local coordinates
660 if (!this.at
.isIdentity()) {
661 final Point2D
.Double p
= inverseTransformPoint(x_p
, y_p
);
664 final Point2D
.Double pd
= inverseTransformPoint(x_d
, y_d
);
667 final Point2D
.Double pdo
= inverseTransformPoint(x_d_old
, y_d_old
);
668 x_d_old
= (int)pdo
.x
;
669 y_d_old
= (int)pdo
.y
;
672 final int tool
= ProjectToolbar
.getToolId();
674 if (ProjectToolbar
.PEN
== tool
|| ProjectToolbar
.PENCIL
== tool
) {
675 //if a point in the backbone is found, then:
676 if (-1 != index
&& !me
.isAltDown() && !me
.isShiftDown()) {
677 dragPoint(index
, x_d
- x_d_old
, y_d
- y_d_old
);
684 public void mouseReleased(MouseEvent me
, int x_p
, int y_p
, int x_d
, int y_d
, int x_r
, int y_r
) {
686 final int tool
= ProjectToolbar
.getToolId();
688 if (ProjectToolbar
.PEN
== tool
|| ProjectToolbar
.PENCIL
== tool
) {
689 repaint(); //needed at least for the removePoint
692 //update points in database if there was any change
695 // update all points, since the index may have changed
696 updateInDatabase("points");
697 } else if (-1 != index
&& index
!= n_points
) { //second condition happens when the last point has been removed
698 // not implemented // updateInDatabase(getUpdatePointForSQL(index));
700 updateInDatabase("points");
701 } else if (index
!= n_points
) { // don't do it when the last point is removed
703 updateInDatabase("points");
705 updateInDatabase("dimensions");
706 } else if (x_r
!= x_p
|| y_r
!= y_p
) {
707 updateInDatabase("dimensions");
713 is_new_point
= false;
717 synchronized protected void calculateBoundingBox(final boolean adjust_position
) {
718 double min_x
= Double
.MAX_VALUE
;
719 double min_y
= Double
.MAX_VALUE
;
723 this.width
= this.height
= 0;
724 layer_set
.updateBucket(this);
728 for (int i
=0; i
<n_points
; i
++) {
729 if (p
[0][i
] < min_x
) min_x
= p
[0][i
];
730 if (p
[1][i
] < min_y
) min_y
= p
[1][i
];
731 if (p
[0][i
] > max_x
) max_x
= p
[0][i
];
732 if (p
[1][i
] > max_y
) max_y
= p
[1][i
];
735 this.width
= max_x
- min_x
;
736 this.height
= max_y
- min_y
;
738 if (adjust_position
) {
739 // now readjust points to make min_x,min_y be the x,y
740 for (int i
=0; i
<n_points
; i
++) {
741 p
[0][i
] -= min_x
; p
[1][i
] -= min_y
;
743 this.at
.translate(min_x
, min_y
); // not using super.translate(...) because a preConcatenation is not needed; here we deal with the data.
744 updateInDatabase("transform");
746 updateInDatabase("dimensions");
748 layer_set
.updateBucket(this);
751 /**Release all memory resources taken by this object.*/
752 synchronized public void destroy() {
758 /**Release memory resources used by this object: namely the arrays of points, which can be reloaded with a call to setupForDisplay()*/
759 synchronized public void flush() {
762 n_points
= -1; // flag that points exist but are not loaded
765 public void repaint() {
769 /**Repaints in the given ImageCanvas only the area corresponding to the bounding box of this Pipe. */
770 public void repaint(boolean repaint_navigator
) {
771 //TODO: this could be further optimized to repaint the bounding box of the last modified segments, i.e. the previous and next set of interpolated points of any given backbone point. This would be trivial if each segment of the Bezier curve was an object.
772 Rectangle box
= getBoundingBox(null);
773 calculateBoundingBox(true);
774 box
.add(getBoundingBox(null));
775 Display
.repaint(layer_set
, this, box
, 5, repaint_navigator
);
778 /**Make this object ready to be painted.*/
779 synchronized private void setupForDisplay() {
781 /* Database storage not implemented yet
783 ArrayList al = project.getLoader().fetchPolylinePoints(id);
784 n_points = al.size();
785 p = new double[2][n_points];
786 p_layer = new long[n_points];
787 Iterator it = al.iterator();
789 while (it.hasNext()) {
790 Object[] ob = (Object[])it.next();
791 p[0][i] = ((Double)ob[0]).doubleValue();
792 p[1][i] = ((Double)ob[1]).doubleValue();
793 p_layer[i] = ((Long)ob[7]).longValue();
800 /** The exact perimeter of this pipe, in integer precision. */
801 synchronized public Polygon
getPerimeter() {
802 if (null == p
|| p
[0].length
< 2) return new Polygon();
804 // local pointers, since they may be transformed
805 int n_points
= this.n_points
;
806 if (!this.at
.isIdentity()) {
807 final Object
[] ob
= getTransformedData();
808 p
= (double[][])ob
[0];
809 n_points
= p
[0].length
;
811 int[] x
= new int[n_points
];
812 int[] y
= new int[n_points
];
813 for (int i
=0; i
<n_points
; i
++) {
817 return new Polygon(x
, y
, n_points
);
820 public boolean isDeletable() {
821 return 0 == n_points
;
824 /** The number of points in this pipe. */
825 public int length() {
826 if (-1 == n_points
) setupForDisplay();
830 synchronized public boolean contains(final Layer layer
, int x
, int y
) {
831 Display front
= Display
.getFront();
834 double mag
= front
.getCanvas().getMagnification();
835 radius
= (10.0D
/ mag
);
836 if (radius
< 2) radius
= 2;
838 // else assume fixed radius of 10 around the line
840 final long lid
= layer
.getId();
841 final double z
= layer
.getZ();
843 for (int i
=0; i
<n_points
; i
++) {
844 if (lid
== p_layer
[i
]) {
846 if (i
> 0 && M
.distancePointToLine(x
, y
, p
[0][i
-1], p
[1][i
-1], p
[0][i
], p
[1][i
]) < radius
) {
849 if (i
< (n_points
-1) && M
.distancePointToLine(x
, y
, p
[0][i
], p
[1][i
], p
[0][i
+1], p
[1][i
+1]) < radius
) {
853 double z1
= layer_set
.getLayer(p_layer
[i
-1]).getZ();
854 double z2
= layer_set
.getLayer(p_layer
[i
]).getZ();
855 if ( (z1
< z
&& z
< z2
)
856 || (z2
< z
&& z
< z1
) ) {
857 // line between j and j-1 crosses the given layer
858 if (M
.distancePointToLine(x
, y
, p
[0][i
-1], p
[1][i
-1], p
[0][i
], p
[1][i
]) < radius
) {
867 /* Scan the Display and link Patch objects that lay under this Pipe's bounding box. */
868 public void linkPatches() { // TODO needs to check all layers!!
869 unlinkAll(Patch
.class);
870 // sort points by layer id
871 final HashMap
<Long
,ArrayList
<Integer
>> m
= new HashMap
<Long
,ArrayList
<Integer
>>();
872 for (int i
=0; i
<n_points
; i
++) {
873 ArrayList
<Integer
> a
= m
.get(p_layer
[i
]);
875 a
= new ArrayList
<Integer
>();
876 m
.put(p_layer
[i
], a
);
880 // For each layer id, search patches whose perimeter includes
881 // one of the backbone points in this path:
882 for (Map
.Entry
<Long
,ArrayList
<Integer
>> e
: m
.entrySet()) {
883 final Layer layer
= layer_set
.getLayer(e
.getKey().longValue());
884 for (Displayable patch
: layer
.getDisplayables(Patch
.class)) {
885 final Polygon perimeter
= patch
.getPerimeter();
886 for (Integer in
: e
.getValue()) {
887 final int i
= in
.intValue();
888 if (perimeter
.contains(p
[0][i
], p
[1][i
])) {
897 /** Returns the layer of lowest Z coordinate where this ZDisplayable has a point in, or the creation layer if no points yet. */
898 public Layer
getFirstLayer() {
899 if (0 == n_points
) return this.layer
;
900 if (-1 == n_points
) setupForDisplay(); //reload
901 Layer la
= this.layer
;
902 double z
= Double
.MAX_VALUE
;
903 for (int i
=0; i
<n_points
; i
++) {
904 Layer layer
= layer_set
.getLayer(p_layer
[i
]);
905 if (layer
.getZ() < z
) la
= layer
;
911 synchronized public void exportXML(StringBuffer sb_body
, String indent
, Object any
) {
912 sb_body
.append(indent
).append("<t2_polyline\n");
913 String in
= indent
+ "\t";
914 super.exportXML(sb_body
, in
, any
);
915 if (-1 == n_points
) setupForDisplay(); // reload
916 //if (0 == n_points) return;
917 String
[] RGB
= Utils
.getHexRGBColor(color
);
918 sb_body
.append(in
).append("style=\"fill:none;stroke-opacity:").append(alpha
).append(";stroke:#").append(RGB
[0]).append(RGB
[1]).append(RGB
[2]).append(";stroke-width:1.0px;stroke-opacity:1.0\"\n");
920 sb_body
.append(in
).append("d=\"M");
921 for (int i
=0; i
<n_points
-1; i
++) {
922 sb_body
.append(" ").append(p
[0][i
]).append(",").append(p
[1][i
]).append(" L");
924 sb_body
.append(" ").append(p
[0][n_points
-1]).append(',').append(p
[1][n_points
-1]).append("\"\n");
925 sb_body
.append(in
).append("layer_ids=\""); // different from 'layer_id' in superclass
926 for (int i
=0; i
<n_points
; i
++) {
927 sb_body
.append(p_layer
[i
]);
928 if (n_points
-1 != i
) sb_body
.append(",");
930 sb_body
.append("\"\n");
932 sb_body
.append(indent
).append(">\n");
933 super.restXML(sb_body
, in
, any
);
934 sb_body
.append(indent
).append("</t2_polyline>\n");
937 /** Exports to type t2_polyline. */
938 static public void exportDTD(StringBuffer sb_header
, HashSet hs
, String indent
) {
939 String type
= "t2_polyline";
940 if (hs
.contains(type
)) return;
942 sb_header
.append(indent
).append("<!ELEMENT t2_polyline (").append(Displayable
.commonDTDChildren()).append(")>\n");
943 Displayable
.exportDTD(type
, sb_header
, hs
, indent
);
944 sb_header
.append(indent
).append(TAG_ATTR1
).append(type
).append(" d").append(TAG_ATTR2
)
948 /** Performs a deep copy of this object, without the links. */
949 synchronized public Displayable
clone(final Project pr
, final boolean copy_id
) {
950 final long nid
= copy_id ?
this.id
: pr
.getLoader().getNextId();
951 final Polyline copy
= new Polyline(pr
, nid
, null != title ? title
.toString() : null, width
, height
, alpha
, this.visible
, new Color(color
.getRed(), color
.getGreen(), color
.getBlue()), this.locked
, (AffineTransform
)this.at
.clone());
953 if (-1 == n_points
) setupForDisplay(); // load data
954 copy
.n_points
= n_points
;
955 copy
.p
= new double[][]{(double[])this.p
[0].clone(), (double[])this.p
[1].clone()};
956 copy
.p_layer
= (long[])this.p_layer
.clone();
957 copy
.addToDatabase();
963 synchronized public List
generateTriangles(double scale
, int parallels
, int resample
) {
964 if (n_points
< 2) return null;
965 // check minimum requirements.
966 if (parallels
< 3) parallels
= 3;
968 final double[][][] all_points
= generateJoints(parallels
, resample
, layer_set
.getCalibrationCopy());
969 return Pipe
.generateTriangles(all_points
, scale
);
972 private double[][][] generateJoints(final int parallels
, final int resample
, final Calibration cal
) {
973 if (-1 == n_points
) setupForDisplay();
975 // local pointers, since they may be transformed
976 int n_points
= this.n_points
;
977 double[][] p
= this.p
;
978 if (!this.at
.isIdentity()) {
979 final Object
[] ob
= getTransformedData();
980 p
= (double[][])ob
[0];
981 n_points
= p
[0].length
;
983 double[] p_width
= new double[n_points
];
984 double[] z_values
= new double[n_points
];
986 for (int i
=0; i
<n_points
; i
++) {
988 z_values
[i
] = layer_set
.getLayer(p_layer
[i
]).getZ();
991 return Pipe
.makeTube(p
[0], p
[1], z_values
, p_width
, resample
, parallels
, cal
);
994 synchronized private Object
[] getTransformedData() {
995 final int n_points
= this.n_points
;
996 final double[][] p
= transformPoints(this.p
, n_points
);
997 return new Object
[]{p
};
1000 public boolean intersects(final Area area
, final double z_first
, final double z_last
) {
1001 if (-1 == n_points
) setupForDisplay();
1002 for (int i
=0; i
<n_points
; i
++) {
1003 final double z
= layer_set
.getLayer(p_layer
[i
]).getZ();
1004 if (z
< z_first
|| z
> z_last
) continue;
1005 if (area
.contains(p
[0][i
], p
[1][i
])) return true;
1010 /** Returns a non-calibrated VectorString3D. */
1011 synchronized public VectorString3D
asVectorString3D() {
1012 // local pointers, since they may be transformed
1013 int n_points
= this.n_points
;
1014 double[][] p
= this.p
;
1015 if (!this.at
.isIdentity()) {
1016 final Object
[] ob
= getTransformedData();
1017 p
= (double[][])ob
[0];
1018 n_points
= p
[0].length
;
1020 double[] z_values
= new double[n_points
];
1021 for (int i
=0; i
<n_points
; i
++) {
1022 z_values
[i
] = layer_set
.getLayer(p_layer
[i
]).getZ();
1025 final double[] px
= p
[0];
1026 final double[] py
= p
[1];
1027 final double[] pz
= z_values
;
1028 VectorString3D vs
= null;
1030 vs
= new VectorString3D(px
, py
, pz
, false);
1031 } catch (Exception e
) { IJError
.print(e
); }
1035 public String
getInfo() {
1036 if (-1 == n_points
) setupForDisplay(); //reload
1040 VectorString3D vs
= asVectorString3D();
1041 vs
.calibrate(this.layer_set
.getCalibration());
1042 len
= vs
.computeLength(); // no resampling
1044 return new StringBuffer("Length: ").append(Utils
.cutNumber(len
, 2, true)).append(' ').append(this.layer_set
.getCalibration().getUnits()).append('\n').toString();
1047 public ResultsTable
measure(ResultsTable rt
) {
1048 if (-1 == n_points
) setupForDisplay(); //reload
1049 if (0 == n_points
) return rt
;
1050 if (null == rt
) rt
= Utils
.createResultsTable("Polyline results", new String
[]{"id", "length", "name-id"});
1053 Calibration cal
= layer_set
.getCalibration();
1055 VectorString3D vs
= asVectorString3D();
1057 len
= vs
.computeLength(); // no resampling
1059 rt
.incrementCounter();
1060 rt
.addLabel("units", cal
.getUnit());
1061 rt
.addValue(0, this.id
);
1062 rt
.addValue(1, len
);
1063 rt
.addValue(2, getNameId());
1067 /** Resample the curve to, first, a number of points as resulting from resampling to a point interdistance of delta, and second, as adjustment by random walk of those points towards the original points. */
1068 static public Object
[] simplify(final double[] px
, final double[] py
, final long[] p_layer_ids
, /*final double delta, final double allowed_error_per_point,*/ final int max_iterations
, final LayerSet layer_set
) throws Exception
{
1069 if (px
.length
!= py
.length
|| py
.length
!= p_layer_ids
.length
) return null;
1071 final double[] pz
= new double[px
.length
];
1072 for (int i
=0; i
<pz
.length
; i
++) {
1073 pz
[i
] = layer_set
.getLayer(p_layer_ids
[i
]).getZ();
1078 VectorString3D vs = new VectorString3D(px, py, pz, false);
1079 Calibration cal = layer_set.getCalibrationCopy();
1082 cal.pixelWidth = 1 / cal.pixelWidth;
1083 cal.pixelHeight = 1 / cal.pixelHeight;
1084 vs.calibrate(cal); // undo it, since source points are in pixels
1086 Pth path = new Pth(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2));
1089 // The above fails with strangely jagged lines.
1092 // Instead, just a line:
1093 Calibration cal
= layer_set
.getCalibrationCopy();
1094 double one_unit
= 1 / cal
.pixelWidth
; // in pixels
1095 double traced_length
= M
.distance(px
[0], py
[0], pz
[0],
1096 px
[px
.length
-1], py
[py
.length
-1], pz
[pz
.length
-1]);
1097 double segment_length
= (one_unit
* 5);
1098 int n_new_points
= (int)(traced_length
/ segment_length
) + 1;
1099 double[] rx
= new double[n_new_points
];
1100 double[] ry
= new double[n_new_points
];
1101 double[] rz
= new double[n_new_points
];
1102 rx
[0] = px
[0]; rx
[rx
.length
-1] = px
[px
.length
-1];
1103 ry
[0] = py
[0]; ry
[ry
.length
-1] = py
[py
.length
-1];
1104 rz
[0] = pz
[0]; rz
[rz
.length
-1] = pz
[pz
.length
-1];
1105 Vector3d v
= new Vector3d(rx
[n_new_points
-1] - rx
[0],
1106 ry
[n_new_points
-1] - ry
[0],
1107 rz
[n_new_points
-1] - rz
[0]);
1109 v
.scale(segment_length
);
1110 for (int i
=1; i
<n_new_points
-1; i
++) {
1111 rx
[i
] = rx
[0] + v
.x
* i
;
1112 ry
[i
] = ry
[0] + v
.y
* i
;
1113 rz
[i
] = rz
[0] + v
.z
* i
;
1115 Pth path
= new Pth(rx
, ry
, rz
);
1116 rx
= ry
= rz
= null;
1118 //final double lowest_error = px.length * allowed_error_per_point;
1120 final Random rand
= new Random(System
.currentTimeMillis());
1122 double current_error
= Double
.MAX_VALUE
;
1124 //double[] er = new double[max_iterations];
1125 //double[] index = new double[max_iterations];
1126 int missed_in_a_row
= 0;
1127 for (; i
<max_iterations
; i
++) {
1128 final Pth copy
= path
.copy().shakeUpOne(d
, rand
);
1129 double error
= copy
.measureErrorSq(px
, py
, pz
);
1130 // If less error, keep the copy
1131 if (error
< current_error
) {
1132 current_error
= error
;
1136 missed_in_a_row
= 0;
1138 //er[i] = current_error;
1141 if (missed_in_a_row
> 10 * path
.px
.length
) {
1142 Utils
.log2("Stopped random walk at iteration " + i
);
1148 // If below lowest_error, quit searching
1149 if (current_error < lowest_error) {
1150 Utils.log2("Stopped at iteration " + i);
1157 Plot plot = new Plot("error", "iteration", "error", index, er);
1158 plot.setLineWidth(2);
1162 if (max_iterations
== i
) {
1163 Utils
.log2("Reached max iterations -- current error: " + current_error
);
1166 // Approximate new Z to a layer id:
1167 long[] plids
= new long[path
.px
.length
];
1168 plids
[0] = p_layer_ids
[0]; // first point untouched
1169 for (int k
=1; k
<path
.pz
.length
; k
++) {
1170 plids
[k
] = layer_set
.getNearestLayer(path
.pz
[k
]).getId();
1173 return new Object
[]{path
.px
, path
.py
, plids
};
1176 /** A path of points in 3D. */
1177 static private class Pth
{
1178 final double[] px
, py
, pz
;
1179 Pth(double[] px
, double[] py
, double[] pz
) {
1184 /** Excludes first and last points. */
1185 final Pth
shakeUpOne(final double d
, final Random rand
) {
1186 final int i
= rand
.nextInt(px
.length
-1) + 1;
1187 px
[i
] += d
* (rand
.nextBoolean() ?
1 : -1);
1188 py
[i
] += d
* (rand
.nextBoolean() ?
1 : -1);
1189 pz
[i
] += d
* (rand
.nextBoolean() ?
1 : -1);
1193 return new Pth( Utils
.copy(px
, px
.length
),
1194 Utils
.copy(py
, py
.length
),
1195 Utils
.copy(pz
, pz
.length
) );
1197 /** Excludes first and last points. */
1198 final double measureErrorSq(final double[] ox
, final double[] oy
, final double[] oz
) {
1200 for (int i
=1; i
<ox
.length
-1; i
++) {
1201 double min_dist
= Double
.MAX_VALUE
;
1202 for (int j
=1; j
<px
.length
; j
++) {
1203 // distance from a original point to a line defined by two consecutive new points
1204 double dist
= M
.distancePointToSegmentSq(ox
[i
], oy
[i
], oz
[i
],
1205 px
[j
-1], py
[j
-1], pz
[j
-1],
1206 px
[j
], py
[j
], pz
[j
]);
1207 if (dist
< min_dist
) min_dist
= dist
;
1215 public void setPosition(FallLine fl
) {
1216 // Where are we now?
1220 final Class
getInternalDataPackageClass() {
1221 return DPPolyline
.class;
1225 synchronized Object
getDataPackage() {
1226 return new DPPolyline(this);
1229 static private final class DPPolyline
extends Displayable
.DataPackage
{
1231 final long[] p_layer
;
1233 DPPolyline(final Polyline polyline
) {
1235 // store copies of all arrays
1236 this.p
= new double[][]{Utils
.copy(polyline
.p
[0], polyline
.n_points
), Utils
.copy(polyline
.p
[1], polyline
.n_points
)};
1237 this.p_layer
= new long[polyline
.n_points
]; System
.arraycopy(polyline
.p_layer
, 0, this.p_layer
, 0, polyline
.n_points
);
1240 final boolean to2(final Displayable d
) {
1242 final Polyline polyline
= (Polyline
)d
;
1243 final int len
= p
[0].length
; // == n_points, since it was cropped on copy
1244 polyline
.p
= new double[][]{Utils
.copy(p
[0], len
), Utils
.copy(p
[1], len
)};
1245 polyline
.n_points
= p
[0].length
;
1246 polyline
.p_layer
= new long[len
]; System
.arraycopy(p_layer
, 0, polyline
.p_layer
, 0, len
);