Major cleanup of Utils class.
[trakem2.git] / ini / trakem2 / display / Polyline.java
blobb9035a5d63072d87e1ef65631bd4f25197356cc9
1 /**
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.
21 **/
23 package ini.trakem2.display;
25 import ij.gui.Plot;
26 import ij.gui.Toolbar;
27 import ij.ImagePlus;
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;
58 import java.util.Map;
59 import java.util.Random;
61 import features.ComputeCurvatures;
62 import tracing.Path;
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);
83 addToDatabase();
84 n_points = 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;
90 this.alpha = alpha;
91 this.visible = visible;
92 this.color = color;
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")) {
105 // parse the points
106 // parse the SVG points data
107 ArrayList al_p = new ArrayList();
108 // M: Move To
109 // L: Line To
110 // sequence is: M p[0][0],p[1][0] L p[0][1],p[1][1] L p[0][2],p[1][2] ...
111 // first point:
112 int i_start = data.indexOf('M');
113 int i_L = data.indexOf('L', i_start+1);
114 int next = 0;
115 while (-1 != i_L) {
116 if (p[0].length == next) enlargeArrays();
117 // parse the point
118 // 'X'
119 int i_comma = data.indexOf(',', i_start+1);
120 p[0][next] = Double.parseDouble(data.substring(i_start+1, i_comma));
121 // 'Y'
122 i_L = data.indexOf('L', i_comma);
123 int i_end = i_L;
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
128 i_start = i_L;
129 next++;
131 n_points = next;
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() {
148 enlargeArrays(5);
150 synchronized protected void enlargeArrays(int n_more) {
151 //catch length
152 int length = p[0].length;
153 //make copies
154 double[][] p_copy = new double[2][length + n_more];
155 long[] p_layer_copy = new long[length + n_more];
156 //copy values
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);
160 //assign them
161 this.p = p_copy;
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) {
167 int index = -1;
168 double d = (10.0D / mag);
169 if (d < 2) d = 2;
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) {
174 min_dist = dist;
175 index = i;
178 return index;
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:
184 if (index < 0) {
185 return;
186 } else if (n_points - 1 == index) {
187 //last point out
188 n_points--;
189 } else {
190 //one point out (but not the last)
191 --n_points;
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.
196 p[1][i] = p[1][i+1];
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;
206 //update in database
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;
213 p[0][index] += dx;
214 p[1][index] += dy;
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);
227 //check array size
228 if (p[0].length == n_points) {
229 enlargeArrays();
231 //decide:
232 if (0 == n_points || 1 == n_points || index + 1 == n_points) {
233 //append at the end
234 p[0][n_points] = x_p;
235 p[1][n_points] = y_p;
236 p_layer[n_points] = layer_id;
237 index = n_points;
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) {
254 //append at the end
255 p[0][n_points] = x_p;
256 p[1][n_points] = y_p;
257 p_layer[n_points] = layer_id;
258 index = n_points;
260 last_autotrace_start = -1;
261 } else {
262 // prepend at the beginning
263 for (int i=n_points-1; i>-1; i--) {
264 p[0][i+1] = p[0][i];
265 p[1][i+1] = p[1][i];
266 p_layer[i+1] = p_layer[i];
268 p[0][0] = x_p;
269 p[1][0] = y_p;
270 p_layer[0] = layer_id;
271 index = 0;
273 if (-1 != last_autotrace_start) last_autotrace_start++;
275 } else {
276 //insert at index:
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)
286 p[0][index] = x_p;
287 p[1][index] = y_p;
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++;
299 //add one up
300 this.n_points++;
302 return index;
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();
308 p[0][next] = px[i];
309 p[1][next] = py[i];
310 p_layer[next] = p_layer_ids[i];
312 n_points += len;
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
320 setupForDisplay();
322 //arrange transparency
323 Composite original_composite = null;
324 if (alpha != 1.0f) {
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.
343 // First point:
344 double z = layer_set.getLayer(p_layer[0]).getZ();
345 boolean paint = true;
346 if (z < z_current) {
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);
353 // Paint half line:
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++) {
365 // Determine color
366 z = layer_set.getLayer(p_layer[i]).getZ();
367 if (z < z_current) {
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();
397 switch (keyCode) {
398 case KeyEvent.VK_D:
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.");
401 return;
403 // Else, remove:
404 final int len = n_points - last_autotrace_start;
405 n_points = last_autotrace_start;
406 last_autotrace_start = -1;
407 repaint(true);
408 Utils.log("Removed " + len + " autotraced points.");
409 return;
410 case KeyEvent.VK_R: // reset tracing
411 tr_map.remove(layer_set);
412 ke.consume();
413 Utils.log("Reset tracing data for Polyline " + this);
414 return;
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;
437 double scale = 1;
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
444 int x_pd = x_p;
445 int y_pd = y_p;
446 if (!this.at.isIdentity()) {
447 final Point2D.Double po = inverseTransformPoint(x_p, y_p);
448 x_p = (int)po.x;
449 y_p = (int)po.y;
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);
460 } else {
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_;
472 if (null == tr_) {
473 synchronized (tr_map) {
474 tr_map.put(layer_set, tr);
478 if (tr.update) {
479 worker[0] = new Worker("Preparing Hessian...") { public void run() {
480 startedWorking();
481 try {
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();
484 //virtual.show();
485 Calibration cal = virtual.getCalibration();
486 double minimumSeparation = 1;
487 if (cal != null) minimumSeparation = Math.min(cal.pixelWidth,
488 Math.min(cal.pixelHeight,
489 cal.pixelDepth));
490 ComputeCurvatures hessian = new ComputeCurvatures(virtual, minimumSeparation, null, cal != null);
491 hessian.run();
493 tr.virtual = virtual;
494 tr.scale = scale;
495 tr.hessian = hessian;
496 tr.update = false;
498 } catch (Exception e) {
499 IJError.print(e);
501 finishedWorking();
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() {
526 startedWorking();
527 try {
528 if (null != worker[0]) {
529 // Wait until hessian is ready
530 worker[0].join();
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,
541 tr.hessian,
542 null == tr.hessian ? 1 : 4,
543 null,
544 null != tr.hessian);
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) {
550 if (!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();
562 tr.tracer.run();
564 final Path result = tr.tracer.getResult();
566 tr.tracer = null;
568 if (null == result) {
569 Utils.log("Finding a path failed"); //: "+
570 // not public //SearchThread.exitReasonStrings[tracer.getExitReason()]);
571 return;
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]) {
582 len--;
583 } else break;
585 // Transform points: undo scale, and bring to this Polyline AffineTransform:
586 final AffineTransform aff = new AffineTransform();
587 /* Inverse order: */
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) {
592 po[j] = pos[0][i];
593 po[j+1] = pos[1][i];
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!
603 pox[i] = po2[j];
604 poy[i] = po2[j+1];
607 // Simplify path: to steps of 5 calibration units, or 5 pixels when not calibrated.
608 if (simplify) {
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];
613 len = pox.length;
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); }
624 finishedWorking();
626 Bureaucrat.createAndStart(worker[1], project);
628 index = -1;
629 return;
633 if (ProjectToolbar.PEN == tool || ProjectToolbar.PENCIL == tool) {
635 if (-1 != index) {
636 if (Utils.isControlDown(me) && me.isShiftDown() && p_layer[index] == Display.getFrontLayer(this.project).getId()) {
637 //delete point
638 removePoint(index);
639 index = -1;
640 repaint(false);
641 return;
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()) {
649 //add a new point
650 index = addPoint(x_p, y_p, layer_id, mag);
651 is_new_point = true;
652 repaint(false);
653 return;
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);
662 x_p = (int)p.x;
663 y_p = (int)p.y;
664 final Point2D.Double pd = inverseTransformPoint(x_d, y_d);
665 x_d = (int)pd.x;
666 y_d = (int)pd.y;
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);
678 repaint(false);
679 return;
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
693 if (-1 != index) {
694 if (is_new_point) {
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));
699 // Instead:
700 updateInDatabase("points");
701 } else if (index != n_points) { // don't do it when the last point is removed
702 // update all
703 updateInDatabase("points");
705 updateInDatabase("dimensions");
706 } else if (x_r != x_p || y_r != y_p) {
707 updateInDatabase("dimensions");
710 repaint(true);
712 // reset
713 is_new_point = false;
714 index = -1;
717 synchronized protected void calculateBoundingBox(final boolean adjust_position) {
718 double min_x = Double.MAX_VALUE;
719 double min_y = Double.MAX_VALUE;
720 double max_x = 0.0D;
721 double max_y = 0.0D;
722 if (0 == n_points) {
723 this.width = this.height = 0;
724 layer_set.updateBucket(this);
725 return;
727 // check the points
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() {
753 super.destroy();
754 p = null;
755 p_layer = null;
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() {
760 p = null;
761 p_layer = null;
762 n_points = -1; // flag that points exist but are not loaded
765 public void repaint() {
766 repaint(true);
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() {
780 // load points
781 /* Database storage not implemented yet
782 if (null == p) {
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();
788 int i = 0;
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();
794 i++;
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++) {
814 x[i] = (int)p[0][i];
815 y[i] = (int)p[1][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();
827 return n_points;
830 synchronized public boolean contains(final Layer layer, int x, int y) {
831 Display front = Display.getFront();
832 double radius = 10;
833 if (null != front) {
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]) {
845 // check both lines:
846 if (i > 0 && M.distancePointToLine(x, y, p[0][i-1], p[1][i-1], p[0][i], p[1][i]) < radius) {
847 return true;
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) {
850 return true;
852 } else if (i > 0) {
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) {
859 return true;
864 return false;
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]);
874 if (null == a) {
875 a = new ArrayList<Integer>();
876 m.put(p_layer[i], a);
878 a.add(i);
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])) {
889 this.link(patch);
890 break;
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;
907 return la;
910 /** Exports data. */
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");
919 if (n_points > 0) {
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;
941 hs.add(type);
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());
952 // The data:
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();
959 return copy;
962 /** Calibrated. */
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++) {
987 p_width[i] = 1;
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;
1007 return false;
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;
1029 try {
1030 vs = new VectorString3D(px, py, pz, false);
1031 } catch (Exception e) { IJError.print(e); }
1032 return vs;
1035 public String getInfo() {
1036 if (-1 == n_points) setupForDisplay(); //reload
1037 // measure length
1038 double len = 0;
1039 if (n_points > 1) {
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"});
1051 // measure length
1052 double len = 0;
1053 Calibration cal = layer_set.getCalibration();
1054 if (n_points > 1) {
1055 VectorString3D vs = asVectorString3D();
1056 vs.calibrate(cal);
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());
1064 return rt;
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();
1077 // Resample:
1078 VectorString3D vs = new VectorString3D(px, py, pz, false);
1079 Calibration cal = layer_set.getCalibrationCopy();
1080 vs.calibrate(cal);
1081 vs.resample(delta);
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));
1087 vs = null;
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]);
1108 v.normalize();
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;
1119 final double d = 1;
1120 final Random rand = new Random(System.currentTimeMillis());
1122 double current_error = Double.MAX_VALUE;
1123 int i = 0;
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;
1133 path = copy;
1134 //er[i] = error;
1135 //index[i] = i;
1136 missed_in_a_row = 0;
1137 } else {
1138 //er[i] = current_error;
1139 //index[i] = i;
1140 missed_in_a_row++;
1141 if (missed_in_a_row > 10 * path.px.length) {
1142 Utils.log2("Stopped random walk at iteration " + i);
1143 break;
1145 continue;
1148 // If below lowest_error, quit searching
1149 if (current_error < lowest_error) {
1150 Utils.log2("Stopped at iteration " + i);
1151 break;
1157 Plot plot = new Plot("error", "iteration", "error", index, er);
1158 plot.setLineWidth(2);
1159 plot.show();
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) {
1180 this.px = px;
1181 this.py = py;
1182 this.pz = 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);
1190 return this;
1192 final Pth copy() {
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) {
1199 double error = 0;
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;
1209 error += min_dist;
1211 return error;
1215 public void setPosition(FallLine fl) {
1216 // Where are we now?
1219 @Override
1220 final Class getInternalDataPackageClass() {
1221 return DPPolyline.class;
1224 @Override
1225 synchronized Object getDataPackage() {
1226 return new DPPolyline(this);
1229 static private final class DPPolyline extends Displayable.DataPackage {
1230 final double[][] p;
1231 final long[] p_layer;
1233 DPPolyline(final Polyline polyline) {
1234 super(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);
1239 @Override
1240 final boolean to2(final Displayable d) {
1241 super.to1(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);
1247 return true;