Use internal SNAPSHOT couplings again
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / Profile.java
blob6c0a2f62b9d78aa4cf6cfe19524d6d08bc0909d2
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 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.measure.Calibration;
26 import ij.measure.ResultsTable;
27 import ini.trakem2.Project;
28 import ini.trakem2.persistence.XMLOptions;
29 import ini.trakem2.tree.ProjectThing;
30 import ini.trakem2.utils.IJError;
31 import ini.trakem2.utils.M;
32 import ini.trakem2.utils.ProjectToolbar;
33 import ini.trakem2.utils.Utils;
34 import ini.trakem2.vector.SkinMaker;
35 import ini.trakem2.vector.VectorString2D;
37 import java.awt.AlphaComposite;
38 import java.awt.Color;
39 import java.awt.Composite;
40 import java.awt.Graphics2D;
41 import java.awt.Polygon;
42 import java.awt.Rectangle;
43 import java.awt.event.KeyEvent;
44 import java.awt.event.MouseEvent;
45 import java.awt.geom.AffineTransform;
46 import java.awt.geom.Area;
47 import java.awt.geom.NoninvertibleTransformException;
48 import java.awt.geom.Point2D;
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.Iterator;
53 import java.util.List;
54 import java.util.Map;
56 import javax.vecmath.Point3f;
59 /** A class to be a user-outlined profile over an image, which is painted with a particular color and also holds an associated text label.
60 * TODO - label not implemented yet.
61 * - can't paint segments in different colors yet
62 * - the whole curve is updated when only a particular set of points needs readjustment
63 * - also, points are smooth, and options should be given to make them non-smooth.
65 public class Profile extends Displayable implements VectorData {
67 /**The number of points.*/
68 protected int n_points;
69 /**The array of clicked points.*/
70 protected double[][] p = new double[2][5];
71 /**The array of left control points, one for each clicked point.*/
72 protected double[][] p_l = new double[2][5];
73 /**The array of right control points, one for each clicked point.*/
74 protected double[][] p_r = new double[2][5];
75 /**The array of interpolated points generated from p, p_l and p_r.*/
76 protected double[][] p_i = new double[2][0];
77 /**Paint/behave as open or closed curve.*/
78 protected boolean closed = false;
80 /** A new user-requested Profile.*/
81 public Profile(Project project, String title, double x, double y) {
82 super(project, title, x, y);
83 n_points = 0;
84 addToDatabase();
87 /**Construct a Bezier Profile object from a set of points mixed in this pattern: PCCPCCPCCPCCP , so, [PCC]n where P = backbone point and C = control point. This results from a BezierApproximation on a path of points as drawed with the mouse. Keep in mind the control points will NOT have the same tangents, but this may be either left like that or corrected with some smoothing algorithm.*/
88 public Profile(Project project, String title, double x, double y, Point2D.Double[] points) {
89 super(project, title, x, y);
90 //setup arrays
91 int size = (points.length / 3) + 1;
92 p = new double[2][size];
93 p_l =new double[2][size];
94 p_r =new double[2][size];
95 //first point:
96 p[0][0] = points[0].x;
97 p[1][0] = points[0].y;
98 p_l[0][0] = p[0][0];
99 p_l[1][0] = p[1][0];
100 p_r[0][0] = points[1].x;
101 p_r[1][0] = points[1].y;
102 n_points++;
103 //all middle points:
104 for (int j=1, i=3; i<points.length-3; i+=3, j++) {
105 p[0][j] = points[i].x;
106 p[1][j] = points[i].y;
107 p_l[0][j] = points[i-1].x;
108 p_l[1][j] = points[i-1].y;
109 if (null == points[i+1]) {
110 Utils.log("BezierProfile: points[" + i + " + 1] is null !");
112 p_r[0][j] = points[i+1].x;
113 p_r[1][j] = points[i+1].y;
114 n_points++;
116 //last point:
117 int last = points.length-1;
118 p[0][n_points] = points[last].x;
119 p[1][n_points] = points[last].y;
120 p_l[0][n_points] = points[last-1].x;
121 p_l[1][n_points] = points[last-1].y;
122 p_r[0][n_points] = p[0][n_points];
123 p_r[1][n_points] = p[1][n_points];
124 n_points++;
126 calculateBoundingBox();
128 //add to database
129 addToDatabase();
130 updateInDatabase("points");
133 /**Construct a Bezier Profile from the database.*/
134 public Profile(Project project, long id, String title, float alpha, boolean visible, Color color, double[][][] bezarr, boolean closed, boolean locked, AffineTransform at) {
135 super(project, id, title, locked, at, 0, 0);
136 this.visible = visible;
137 this.alpha = alpha;
138 this.color = color;
139 this.closed = closed;
140 //make points from the polygon, in which they are stored as LPRLPR ... left control - backbone point - right control point (yes this looks very much like codons, but the important bit is the middle one, not the left one! HaHaHaHa!)
141 //// points are fields x,y in PGpoint.
142 p_l = bezarr[0];
143 p = bezarr[1];
144 p_r = bezarr[2];
145 n_points = p[0].length;
147 //calculate width and height
148 calculateBoundingBox(false);
151 /** Construct a Bezier Profile from the database, but the points will be loaded later, when actually needed, by calling setupForDisplay(). */
152 public Profile(Project project, long id, String title, float width, float height, float alpha, boolean visible, Color color, boolean closed, boolean locked, AffineTransform at) {
153 super(project, id, title, locked, at, width, height);
154 this.visible = visible;
155 this.alpha = alpha;
156 this.color = color;
157 this.closed = closed;
158 this.n_points = -1; //used as a flag to signal "I have points, but unloaded"
161 /** Construct a Bezier Profile from an XML entry. */
162 public Profile(Project project, long id, HashMap<String,String> ht, HashMap<Displayable,String> ht_links) {
163 super(project, id, ht, ht_links);
164 // parse data
165 for (final Map.Entry<String,String> entry : ht.entrySet()) {
166 final String key = entry.getKey();
167 final String data = entry.getValue();
168 if (key.equals("d")) {
169 // parse the SVG points data
170 final ArrayList<String> al_p = new ArrayList<String>(),
171 al_p_r = new ArrayList<String>(),
172 al_p_l = new ArrayList<String>();// needs shifting, inserting one point at the beginning if not closed.
173 // sequence is: M p[0],p[1] C p_r[0],p_r[1] p_l[0],p_l[1] and repeat without the M, and finishes with the last p[0],p[1]. If closed, appended at the end is p_r[0],p_r[1] p_l[0],p_l[1]
174 // first point:
175 int i_start = data.indexOf('M');
176 int i_end = data.indexOf('C');
177 String point = data.substring(i_start+1, i_end).trim();
178 al_p.add(point);
179 boolean go = true;
180 while (go) {
181 i_start = i_end;
182 i_end = data.indexOf('C', i_end+1);
183 if (-1 == i_end) {
184 i_end = data.length() -1;
185 go = false;
187 String txt = data.substring(i_start+1, i_end).trim();
188 // eliminate double spaces
189 while (-1 != txt.indexOf(" ")) {
190 txt = txt.replaceAll(" ", " ");
192 // reduce ", " and " ," to ","
193 txt = txt.replaceAll(" ,", ",");
194 txt = txt.replaceAll(", ", ",");
195 // cut by spaces
196 String[] points = txt.split(" ");
197 // debug:
198 //Utils.log("Profile init: txt=__" + txt + "__ points.length=" + points.length);
199 if (3 == points.length) {
200 al_p_r.add(points[0]);
201 al_p_l.add(points[1]);
202 al_p.add(points[2]);
203 } else {
204 // error
205 Utils.log("Profile constructor from XML: error at parsing points.");
207 // example: C 34.5,45.6 45.7,23.0 34.8, 78.0 C ..
209 // NO, usability problems // this.closed = (-1 != data.lastIndexOf('z')); // 'z' must be lowercase to comply with SVG style
210 if (this.closed) {
211 // prepend last left control point and delete from the end
212 al_p_l.add(0, al_p_l.remove(al_p_l.size() -1));
213 // remove last point (duplicated in this SVG format)
214 al_p.remove(al_p.size() -1); // TODO check that it's really closed by comparing the points, not just by the z, and if not closed, remove the control points.
215 } else {
216 // prepend a left control point equal to the first point
217 al_p_l.add(0, al_p.get(0)); // no need to clone, String is final
218 // and append a right control point equal to the last point
219 al_p_r.add(al_p.get(al_p.size() -1));
221 // sanity check:
222 if (!(al_p.size() == al_p_l.size() && al_p_l.size() == al_p_r.size())) {
223 Utils.log2("Profile XML parsing: Disagreement in the number of points:\n\tp.length=" + al_p.size() + "\n\tp_l.length=" + al_p_l.size() + "\n\tp_r.length=" + al_p_r.size());
225 // Now parse the points
226 this.n_points = al_p.size();
227 this.p = new double[2][n_points];
228 this.p_l = new double[2][n_points];
229 this.p_r = new double[2][n_points];
230 for (int i=0; i<n_points; i++) {
231 String[] sp = al_p.get(i).split(",");
232 p[0][i] = Double.parseDouble(sp[0]);
233 p[1][i] = Double.parseDouble(sp[1]);
234 sp = al_p_l.get(i).split(",");
235 p_l[0][i] = Double.parseDouble(sp[0]);
236 p_l[1][i] = Double.parseDouble(sp[1]);
237 sp = al_p_r.get(i).split(",");
238 p_r[0][i] = Double.parseDouble(sp[0]);
239 p_r[1][i] = Double.parseDouble(sp[1]);
241 this.p_i = new double[2][0]; // empty
242 generateInterpolatedPoints(0.05);
247 /** A constructor for cloning purposes. */
248 private Profile(Project project, String title, double x, double y, float width, float height, float alpha, Color color, int n_points, double[][] p, double[][] p_r, double[][] p_l, double[][] p_i, boolean closed) {
249 super(project, title, x, y);
250 this.width = width;
251 this.height = height;
252 this.alpha = alpha;
253 this.color = color;
254 this.n_points = n_points;
255 this.p = p;
256 this.p_r = p_r;
257 this.p_l = p_l;
258 this.p_i = p_i;
259 this.closed = closed;
260 addToDatabase();
261 updateInDatabase("all");
264 /**Increase the size of the arrays by 5 points.*/
265 protected void enlargeArrays() {
266 //catch length
267 int length = p[0].length;
268 //make copies
269 double[][] p_copy = new double[2][length + 5];
270 double[][] p_l_copy = new double[2][length + 5];
271 double[][] p_r_copy = new double[2][length + 5];
272 //copy values
273 System.arraycopy(p[0], 0, p_copy[0], 0, length);
274 System.arraycopy(p[1], 0, p_copy[1], 0, length);
275 System.arraycopy(p_l[0], 0, p_l_copy[0], 0, length);
276 System.arraycopy(p_l[1], 0, p_l_copy[1], 0, length);
277 System.arraycopy(p_r[0], 0, p_r_copy[0], 0, length);
278 System.arraycopy(p_r[1], 0, p_r_copy[1], 0, length);
279 //assign them
280 this.p = p_copy;
281 this.p_l = p_l_copy;
282 this.p_r = p_r_copy;
285 /** Returns the number of backbone points. */
286 public int getPointCount() {
287 return n_points;
290 public boolean isClosed() {
291 return this.closed;
294 /**Find a point in an array 'a', with a precision dependent on the magnification. */
295 protected int findPoint(double[][] a, double x_p, double y_p, double magnification) {
296 int index = -1;
297 // make parameters local
298 double d = (10.0D / magnification);
299 if (d < 2) d = 2;
300 for (int i=0; i<n_points; i++) {
301 if ((Math.abs(x_p - a[0][i]) + Math.abs(y_p - a[1][i])) <= d) {
302 index = i;
305 return index;
308 /**Remove a point from the bezier backbone and its two associated control points.*/
309 protected void removePoint(int index) {
310 // check preconditions:
311 if (index < 0) {
312 return;
313 } else if (n_points - 1 == index) {
314 //last point out
315 n_points--;
316 } else {
317 //one point out (but not the last)
318 n_points--;
319 // shift all points after 'index' one position to the left:
320 for (int i=index; i<n_points; i++) {
321 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.
322 p[1][i] = p[1][i+1];
323 p_l[0][i] = p_l[0][i+1];
324 p_l[1][i] = p_l[1][i+1];
325 p_r[0][i] = p_r[0][i+1];
326 p_r[1][i] = p_r[1][i+1];
329 // open the curve if necessary
330 if (closed && n_points < 2) {
331 closed = false;
332 updateInDatabase("closed");
334 //update in database
335 updateInDatabase("points");
338 /**Calculate distance from one point to another.*/
339 protected double distance(double x1, double y1, double x2, double y2) {
340 return Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
343 /**Move backbone point.*/
344 protected void dragPoint(int index, int dx, int dy) {
345 p[0][index] += dx;
346 p[1][index] += dy;
347 p_l[0][index] += dx;
348 p_l[1][index] += dy;
349 p_r[0][index] += dx;
350 p_r[1][index] += dy;
353 /**Set the control points to the same value as the backbone point which they control.*/
354 protected void resetControlPoints(int index) {
355 p_l[0][index] = p[0][index];
356 p_l[1][index] = p[1][index];
357 p_r[0][index] = p[0][index];
358 p_r[1][index] = p[1][index];
361 /**Drag a control point and adjust the other, dependent one, in a symmetric way or not.*/
362 protected void dragControlPoint(int index, double x_d, double y_d, double[][] p_dragged, double[][] p_adjusted, boolean symmetric) {
363 //measure hypothenusa: from p to p control
364 double hypothenusa;
365 if (symmetric) {
366 //make both points be dragged in parallel, the same distance
367 hypothenusa = distance(p[0][index], p[1][index], p_dragged[0][index], p_dragged[1][index]);
368 } else {
369 //make each point be dragged with its own distance
370 hypothenusa = distance(p[0][index], p[1][index], p_adjusted[0][index], p_adjusted[1][index]);
372 //measure angle: use the point being dragged
373 double angle = Math.atan2(p_dragged[0][index] - p[0][index], p_dragged[1][index] - p[1][index]) + Math.PI;
374 //apply
375 p_dragged[0][index] = x_d;
376 p_dragged[1][index] = y_d;
377 p_adjusted[0][index] = p[0][index] + hypothenusa * Math.sin(angle); // it's sin and not cos because of stupid Math.atan2 way of delivering angles
378 p_adjusted[1][index] = p[1][index] + hypothenusa * Math.cos(angle);
381 /**Add a point either at the end or between two existing points, with accuracy depending on magnification. Does not update the database. x_p,y_p are in the local space. */
382 protected int addPoint(double x_p, double y_p, double magnification, double bezier_finess) {
383 //lookup closest interpolated point and then get the closest clicked point to it
384 int index = findClosestPoint(x_p, y_p, magnification, bezier_finess); // x_p, y_p are already local.
385 if (closed && -1 == index) {
386 return -1;
388 //check array size
389 if (p[0].length == n_points) {
390 enlargeArrays();
392 //decide:
393 if (0 == n_points || 1 == n_points || -1 == index || index + 1 == n_points) {
394 //append at the end
395 p[0][n_points] = p_l[0][n_points] = p_r[0][n_points] = x_p;
396 p[1][n_points] = p_l[1][n_points] = p_r[1][n_points] = y_p;
397 index = n_points;
398 } else {
399 //insert at index:
400 index++; //so it is added after the closest point;
401 // 1 - copy second half of array
402 int sh_length = n_points -index;
403 double[][] p_copy = new double[2][sh_length];
404 double[][] p_l_copy = new double[2][sh_length];
405 double[][] p_r_copy = new double[2][sh_length];
406 System.arraycopy(p[0], index, p_copy[0], 0, sh_length);
407 System.arraycopy(p[1], index, p_copy[1], 0, sh_length);
408 System.arraycopy(p_l[0], index, p_l_copy[0], 0, sh_length);
409 System.arraycopy(p_l[1], index, p_l_copy[1], 0, sh_length);
410 System.arraycopy(p_r[0], index, p_r_copy[0], 0, sh_length);
411 System.arraycopy(p_r[1], index, p_r_copy[1], 0, sh_length);
412 // 2 - insert value into 'p' (the two control arrays get the same value)
413 p[0][index] = p_l[0][index] = p_r[0][index] = x_p;
414 p[1][index] = p_l[1][index] = p_r[1][index] = y_p;
415 // 3 - copy second half into the array
416 System.arraycopy(p_copy[0], 0, p[0], index+1, sh_length);
417 System.arraycopy(p_copy[1], 0, p[1], index+1, sh_length);
418 System.arraycopy(p_l_copy[0], 0, p_l[0], index+1, sh_length);
419 System.arraycopy(p_l_copy[1], 0, p_l[1], index+1, sh_length);
420 System.arraycopy(p_r_copy[0], 0, p_r[0], index+1, sh_length);
421 System.arraycopy(p_r_copy[1], 0, p_r[1], index+1, sh_length);
423 //add one up
424 this.n_points++;
426 // set the x,y and readjust points
427 calculateBoundingBox();
429 return index;
432 /**Find the closest point to an interpolated point with precision depending upon magnification. The point x_p,y_p is in local coordinates. */
433 protected int findClosestPoint(double x_p, double y_p, double magnification, double bezier_finess) {
434 if (0 == p_i[0].length) return -1; // when none added yet
435 int index = -1;
436 double distance_sq = Double.MAX_VALUE;
437 double distance_sq_i;
438 double max = 12.0D / magnification;
439 max = max * max; //squaring it
440 for (int i=0; i<p_i[0].length; i++) {
441 //see which point is closer (there's no need to calculate the distance by multiplying squares and so on).
442 distance_sq_i = (p_i[0][i] - x_p)*(p_i[0][i] - x_p) + (p_i[1][i] - y_p)*(p_i[1][i] - y_p);
443 if (distance_sq_i < max && distance_sq_i < distance_sq) {
444 index = i;
445 distance_sq = distance_sq_i;
448 if (-1 != index) {
449 int index_found = (int)((double)index * bezier_finess);
450 if (index < (index_found / bezier_finess)) {
451 index_found--;
453 index = index_found;
455 return index;
458 /**Toggle curve closed/open.*/
459 public void toggleClosed() {
460 if (closed) {
461 closed = false;
462 } else {
463 closed = true;
465 //update database
466 updateInDatabase("closed");
469 protected void generateInterpolatedPoints(double bezier_finess) {
470 if (0 >= n_points) {
471 return;
474 int n = n_points;
475 if (closed && n > 1) {
476 //do the loop for one more
477 n++;
478 //enlarge arrays if needed
479 if (p[0].length == n_points) {
480 enlargeArrays();
482 //add first point to the end (doesn't need to be deleted because n_points hasn't changed)
483 // n_points works as an index here.
484 p[0][n_points] = p[0][0];
485 p[1][n_points] = p[1][0];
486 p_l[0][n_points] = p_l[0][0];
487 p_l[1][n_points] = p_l[1][0];
488 p_r[0][n_points] = p_r[0][0];
489 p_r[1][n_points] = p_r[1][0];
492 // case there's only one point
493 if (1 == n_points) {
494 p_i = new double[2][1];
495 p_i[0][0] = p[0][0];
496 p_i[1][0] = p[1][0];
497 return;
499 // case there's more: interpolate!
500 p_i = new double[2][(int)(n * (1.0D/bezier_finess))];
501 double t, f0, f1, f2, f3;
502 int next = 0;
503 for (int i=0; i<n-1; i++) {
504 for (t=0.0D; t<1.0D; t += bezier_finess) {
505 f0 = (1-t)*(1-t)*(1-t);
506 f1 = 3*t*(1-t)*(1-t);
507 f2 = 3*t*t*(1-t);
508 f3 = t*t*t;
509 p_i[0][next] = f0*p[0][i] + f1*p_r[0][i] + f2*p_l[0][i+1] + f3*p[0][i+1];
510 p_i[1][next] = f0*p[1][i] + f1*p_r[1][i] + f2*p_l[1][i+1] + f3*p[1][i+1];
511 next++;
512 //enlarge if needed (when bezier_finess is not 0.05, it's difficult to predict because of int loss of precision.
513 if (p_i[0].length == next) {
514 double[][] p_i_copy = new double[2][p_i[0].length + 5];
515 System.arraycopy(p_i[0], 0, p_i_copy[0], 0, p_i[0].length);
516 System.arraycopy(p_i[1], 0, p_i_copy[1], 0, p_i[1].length);
517 p_i = p_i_copy;
521 if (p_i[0].length != next) { // 'next' works as a length here
522 //resize back
523 double[][] p_i_copy = new double[2][next];
524 System.arraycopy(p_i[0], 0, p_i_copy[0], 0, next);
525 System.arraycopy(p_i[1], 0, p_i_copy[1], 0, next);
526 p_i = p_i_copy;
530 @Override
531 public void paint(final Graphics2D g, final Rectangle srcRect, final double magnification, final boolean active, final int channels, final Layer active_layer, final List<Layer> layers) {
532 if (0 == n_points) return;
533 if (-1 == n_points) {
534 // load points from the database
535 setupForDisplay();
536 if (-1 == n_points) {
537 Utils.log2("Profile.paint: Some error ocurred, can't load points from database.");
538 return;
541 //arrange transparency
542 Composite original_composite = null;
543 if (alpha != 1.0f) {
544 original_composite = g.getComposite();
545 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
548 // local pointers, since they may be transformed
549 double[][] p = this.p;
550 double[][] p_r = this.p_r;
551 double[][] p_l = this.p_l;
552 double[][] p_i = this.p_i;
553 if (!this.at.isIdentity()) {
554 final Object[] ob = getTransformedData();
555 p = (double[][])ob[0];
556 p_l = (double[][])ob[1];
557 p_r = (double[][])ob[2];
558 p_i = (double[][])ob[3];
560 if (active) {
561 //draw/fill points
562 final int oval_radius = (int)Math.ceil(4 / magnification);
563 final int oval_corr = (int)Math.ceil(3 / magnification);
564 for (int j=0; j<n_points; j++) {
565 DisplayCanvas.drawHandle(g, (int)p[0][j], (int)p[1][j], magnification);
566 g.setColor(this.color);
567 //fill small ovals at control points
568 g.fillOval((int)p_l[0][j] -oval_corr, (int)p_l[1][j] -oval_corr, oval_radius, oval_radius);
569 g.fillOval((int)p_r[0][j] -oval_corr, (int)p_r[1][j] -oval_corr, oval_radius, oval_radius);
570 //draw lines between backbone and control points
571 g.drawLine((int)p[0][j], (int)p[1][j], (int)p_l[0][j], (int)p_l[1][j]);
572 g.drawLine((int)p[0][j], (int)p[1][j], (int)p_r[0][j], (int)p_r[1][j]);
576 //set color
577 g.setColor(this.color);
579 //draw lines between any two consecutive interpolated points
580 for (int i=0; i<p_i[0].length-1; i++) {
581 g.drawLine((int)p_i[0][i], (int)p_i[1][i], (int)p_i[0][i+1], (int)p_i[1][i+1]);
583 //draw last segment between last and first points, only if closed:
584 if (closed) {
585 g.drawLine((int)p_i[0][p_i[0].length-1], (int)p_i[1][p_i[0].length-1], (int)p_i[0][0], (int)p_i[1][0]);
588 //Transparency: fix alpha composite back to original.
589 if (null != original_composite) {
590 g.setComposite(original_composite);
594 /**Helper vars for mouse events. It's safe to have them static since only one Profile will be edited at a time.*/
595 static private int index = -1;
596 static private int index_l = -1;
597 static private int index_r = -1;
598 static private boolean is_new_point = false;
600 /**Execute the mousePressed MouseEvent on this Profile.*/
601 public void mousePressed(MouseEvent me, Layer layer, int x_p, int y_p, double mag) {
602 // transform the x_p, y_p to the local coordinates
603 if (!this.at.isIdentity()) {
604 final Point2D.Double po = inverseTransformPoint(x_p, y_p);
605 x_p = (int)po.x;
606 y_p = (int)po.y;
609 final int tool = ProjectToolbar.getToolId();
611 // reset helper vars
612 is_new_point = false;
613 index = index_r = index_l = -1;
615 if (ProjectToolbar.PEN == tool) {
617 //collect vars
618 if (Utils.isControlDown(me) && me.isShiftDown()) {
619 index = findNearestPoint(p, n_points, x_p, y_p);
620 } else {
621 index = findPoint(p, x_p, y_p, mag);
624 if (-1 != index) {
625 if (Utils.isControlDown(me) && me.isShiftDown()) {
626 //delete point
627 removePoint(index);
628 index = index_r = index_l = -1;
629 generateInterpolatedPoints(0.05);
630 repaint(false);
631 return;
632 } else if (me.isAltDown()) {
633 resetControlPoints(index);
634 return;
635 } else if (me.isShiftDown()) {
636 if (0 == index && n_points > 1 && !closed) {
637 //close curve, reset left control point of the first point and set it up for dragging
638 closed = true;
639 updateInDatabase("closed");
640 p_l[0][0] = p[0][0];
641 p_l[1][0] = p[1][0];
642 index = -1;
643 index_r = -1;
644 index_l = 0; //the first one
645 repaint(false);
646 return;
651 // find if click is on a left control point
652 index_l = findPoint(p_l, x_p, y_p, mag);
653 index_r = -1;
654 // if not, then try on the set of right control points
655 if (-1 == index_l) {
656 index_r = findPoint(p_r, x_p, y_p, mag);
659 //if no conditions are met, attempt to add point (won't get added if the curve is closed and the click is too far from the interpolated points).
660 if (-1 == index && -1 == index_l && -1 == index_r && !me.isShiftDown() && !me.isAltDown()) {
661 //add a new point and assign its index to the left control point, so it can be dragged right away. This is copying the way Karbon does for drawing Bezier curves, which is the contrary to Adobe's way, but which I find more useful.
662 index_l = addPoint(x_p, y_p, mag, 0.05);
663 if (-1 != index_l) is_new_point = true;
664 else if (1 == n_points) {
665 //for the very first point, drag the right control point, not the left.
666 index_r = index_l;
667 index_l = -1;
669 repaint(false);
670 return;
675 /**Execute the mouseDragged MouseEvent on this Profile.*/
676 public void mouseDragged(MouseEvent me, Layer layer, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old) {
677 // transform to the local coordinates
678 if (!this.at.isIdentity()) {
679 final Point2D.Double p = inverseTransformPoint(x_p, y_p);
680 x_p = (int)p.x;
681 y_p = (int)p.y;
682 final Point2D.Double pd = inverseTransformPoint(x_d, y_d);
683 x_d = (int)pd.x;
684 y_d = (int)pd.y;
685 final Point2D.Double pdo = inverseTransformPoint(x_d_old, y_d_old);
686 x_d_old = (int)pdo.x;
687 y_d_old = (int)pdo.y;
690 final int tool = ProjectToolbar.getToolId();
692 if (ProjectToolbar.PEN == tool) {
694 //if a point in the backbone is found, then:
695 if (-1 != index) {
696 if (!me.isAltDown()) {
697 //drag point
698 dragPoint(index, x_d - x_d_old, y_d - y_d_old);
699 } else {
700 //drag both control points symmetrically
701 dragControlPoint(index, x_d, y_d, p_l, p_r, true);
703 generateInterpolatedPoints(0.05);
704 repaint(false);
705 return;
708 //if a control point is found, then drag it, adjusting the other control point non-symmetrically
709 if (-1 != index_r) {
710 dragControlPoint(index_r, x_d, y_d, p_r, p_l, is_new_point);
711 generateInterpolatedPoints(0.05);
712 repaint(false);
713 return;
715 if (-1 != index_l) {
716 dragControlPoint(index_l, x_d, y_d, p_l, p_r, is_new_point);
717 generateInterpolatedPoints(0.05);
718 repaint(false);
719 return;
722 // no points selected. Drag the whole curve on alt down (without affecting linked curves)
723 if (me.isAltDown()) {
724 int dx = x_d - x_d_old;
725 int dy = y_d - y_d_old;
726 this.at.translate(dx, dy);
727 repaint(false);
728 return;
733 /**Execute the mouseReleased MouseEvent on this Profile.*/
734 public void mouseReleased(MouseEvent me, Layer layer, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r) {
735 int tool = ProjectToolbar.getToolId();
736 if (ProjectToolbar.PEN == tool) {
737 //generate interpolated points
738 generateInterpolatedPoints(0.05);
739 repaint(); //needed at least for the removePoint, and also for repainting the DisplayablePanel and the DisplayNavigator // TODO this may be redundant with below
742 //update points in database if there was any change
743 if (-1 != index || -1 != index_r || -1 != index_l) {
744 updateInDatabase("points");
745 updateInDatabase("transform+dimensions"); //was: dimensions
746 Display.repaint(layer, this); // the DisplayablePanel
747 } else if (x_r != x_p || y_r != y_p) {
748 updateInDatabase("transform+dimensions");
749 Display.repaint(layer, this); // the DisplayablePanel
751 // reset helper vars
752 is_new_point = false;
753 index = index_r = index_l = -1;
756 protected void calculateBoundingBox() {
757 calculateBoundingBox(true);
760 /**Calculate the bounding box of the curve in the shape of a Rectangle defined by x,y,width,height. If adjust_position is true, then points are made local to the minimal x,y. */
761 protected void calculateBoundingBox(boolean adjust_position) {
762 if (0 == n_points) {
763 this.width = this.height = 0;
764 updateBucket();
765 return;
767 //go over all points and control points and find the max and min
768 // (there's no need to use the interpolated points because the points and control points define the boxes in which the interpolated points are).
769 double min_x = Double.MAX_VALUE;
770 double min_y = Double.MAX_VALUE;
771 double max_x = 0.0D;
772 double max_y = 0.0D;
774 for (int i=0; i<n_points; i++) {
775 if (p[0][i] < min_x) min_x = p[0][i];
776 if (p_l[0][i] < min_x) min_x = p_l[0][i];
777 if (p_r[0][i] < min_x) min_x = p_r[0][i];
778 if (p[1][i] < min_y) min_y = p[1][i];
779 if (p_l[1][i] < min_y) min_y = p_l[1][i];
780 if (p_r[1][i] < min_y) min_y = p_r[1][i];
781 if (p[0][i] > max_x) max_x = p[0][i];
782 if (p_l[0][i] > max_x) max_x = p_l[0][i];
783 if (p_r[0][i] > max_x) max_x = p_r[0][i];
784 if (p[1][i] > max_y) max_y = p[1][i];
785 if (p_l[1][i] > max_y) max_y = p_l[1][i];
786 if (p_r[1][i] > max_y) max_y = p_r[1][i];
789 this.width = (float)(max_x - min_x);
790 this.height = (float)(max_y - min_y);
792 if (adjust_position) {
793 // now readjust points to make min_x,min_y be the x,y
794 for (int i=0; i<n_points; i++) {
795 p[0][i] -= min_x; p[1][i] -= min_y;
796 p_l[0][i] -= min_x; p_l[1][i] -= min_y;
797 p_r[0][i] -= min_x; p_r[1][i] -= min_y;
799 for (int i=0; i<p_i[0].length; i++) {
800 p_i[0][i] -= min_x; p_i[1][i] -= min_y;
802 this.at.translate(min_x, min_y); // not using super.translate(...) because a preConcatenation is not needed; here we deal with the data.
803 updateInDatabase("transform");
805 updateBucket();
806 updateInDatabase("dimensions");
810 public void repaint() {
811 repaint(true);
814 /**Repaints in the given ImageCanvas only the area corresponding to the bounding box of this Profile. */
815 public void repaint(boolean repaint_navigator) {
816 //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.
817 Rectangle box = getBoundingBox(null);
818 calculateBoundingBox();
819 box.add(getBoundingBox(null));
820 Display.repaint(layer, this, box, 5, repaint_navigator);
823 /**Check if the given point (usually from a MOUSE_PRESSED MouseEvent) is contained within the boundaries of this object. The point is expected as local coordinates. */
824 public boolean containsPoint(final int x_p, final int y_p) {
825 // as in getPerimeter():
826 int n_i = p_i[0].length;
827 int[] intx = new int[n_i];
828 int[] inty = new int[n_i];
829 for (int i=0; i<n_i; i++) {
830 intx[i] = (int)p_i[0][i];
831 inty[i] = (int)p_i[1][i];
833 Polygon polygon = new Polygon(intx, inty, n_i);
834 return polygon.contains(x_p, y_p);
837 /**Release all memory resources taken by this object.*/
838 public void destroy() {
839 super.destroy();
840 p = null;
841 p_l = null;
842 p_r = null;
843 p_i = null;
846 /**Make this object ready to be painted.*/
847 private void setupForDisplay() {
848 // load points
849 if (null == p || null == p_l || null == p_r) {
850 //load points from database
851 double[][][] bezarr = project.getLoader().fetchBezierArrays(this.id);
852 if (null == bezarr) {
853 Utils.log("Profile.setupForDisplay: could not load the bezier points from the database for id=" + this.id);
854 this.p_i = new double[0][0];
855 return;
857 p_l = bezarr[0];
858 p = bezarr[1];
859 p_r = bezarr[2];
860 n_points = p[0].length;
861 // recreate interpolated points
862 generateInterpolatedPoints(0.05); //TODO the 0.05 bezier finess, read the value from the Project perhaps.
866 /**Cache this Profile if needed.*/ //Is there much sense in caching Profile objects? Unless you have thousands ... and that CAN be the case!
867 public void cache() {
868 //TODO
871 /**Release memory resources used by this object: namely the arrays of points, which can be reloaded with a call to setupForDisplay()*/
872 public void flush() {
873 p = null;
874 p_l = null;
875 p_r = null;
876 p_i = null;
877 n_points = -1; // flag that points exist (and need to be reloaded)
880 /** The perimeter of this profile, in integer precision. */
881 public Polygon getPerimeter() {
882 if (-1 == n_points) setupForDisplay();
883 if (null == p_i) return null; // has been flushed, incorrect access! This is a patch.
885 // transform
886 double[][] p_i = this.p_i;
887 if (!this.at.isIdentity()) p_i = transformPoints(this.p_i);
889 int n_i = p_i[0].length;
890 int[] intx = new int[n_i];
891 int[] inty = new int[n_i];
892 for (int i=0; i<n_i; i++) {
893 intx[i] = (int)p_i[0][i];
894 inty[i] = (int)p_i[1][i];
896 return new Polygon(intx, inty, n_i);
899 /** Writes the data of this object as a Bezier object in the .shapes file represented by the 'data' StringBuffer. The z_scale is added to manually correct for sample squashing under the coverslip. */
900 public void toShapesFile(StringBuffer data, String group, String color, double z_scale) {
901 if (-1 == n_points) setupForDisplay(); // reload
902 // local pointers, since they may be transformed
903 double[][] p = this.p;
904 double[][] p_r = this.p_r;
905 double[][] p_l = this.p_l;
906 if (!this.at.isIdentity()) {
907 final Object[] ob = getTransformedData();
908 p = (double[][])ob[0];
909 p_l = (double[][])ob[1];
910 p_r = (double[][])ob[2];
912 final double z = layer.getZ();
913 final char l = '\n';
914 data.append("type=bezier").append(l)
915 .append("name=").append(project.getMeaningfulTitle(this)).append(l)
916 .append("group=").append(group).append(l)
917 .append("color=").append(color).append(l)
918 .append("supergroup=").append("null").append(l)
919 .append("supercolor=").append("null").append(l)
920 .append("in slice=").append(z * z_scale).append(l) // fake, this is now the absolute z coordinate
921 .append("curve_closed=").append(true).append(l) // must!
922 .append("density field=").append(false).append(l) // must!
924 for (int i=0; i<n_points; i++) {
925 data.append("p x=").append(p[0][i]).append(l)
926 .append("p y=").append(p[1][i]).append(l)
927 .append("p_r x=").append(p_r[0][i]).append(l)
928 .append("p_r y=").append(p_r[1][i]).append(l)
929 .append("p_l x=").append(p_l[0][i]).append(l)
930 .append("p_l y=").append(p_l[1][i]).append(l)
935 public void exportSVG(StringBuffer data, double z_scale, String indent) {
936 String in = indent + "\t";
937 if (-1 == n_points) setupForDisplay(); // reload
938 if (0 == n_points) return;
939 String[] RGB = Utils.getHexRGBColor(color);
940 final double[] a = new double[6];
941 at.getMatrix(a);
942 data.append(indent).append("<path\n")
943 .append(in).append("type=\"profile\"\n")
944 .append(in).append("id=\"").append(id).append("\"\n")
945 .append(in).append("transform=\"matrix(").append(a[0]).append(',')
946 .append(a[1]).append(',')
947 .append(a[2]).append(',')
948 .append(a[3]).append(',')
949 .append(a[4]).append(',')
950 .append(a[5]).append(")\"\n")
951 .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;\"\n")
952 .append(in).append("d=\"M");//.append(p[0][0]).append(',').append(p[1][0]).append(" C ").append(p_r[0][0]).append(',').append(p_r[1][0]);
953 for (int i=0; i<n_points-1; i++) {
954 data.append(' ').append(p[0][i]).append(',').append(p[1][i])
955 .append(" C ").append(p_r[0][i]).append(',').append(p_r[1][i])
956 .append(' ').append(p_l[0][i+1]).append(',').append(p_l[1][i+1])
959 data.append(' ').append(p[0][n_points-1]).append(',').append(p[1][n_points-1]);
960 if (closed) {
961 data.append(" C ").append(p_r[0][n_points-1]).append(',').append(p_r[1][n_points-1])
962 .append(' ').append(p_l[0][0]).append(',').append(p_l[1][0])
963 .append(' ').append(p[0][0]).append(',').append(p[1][0])
964 .append(" z")
967 data.append("\"\n")
968 .append(in).append("z=\"").append(layer.getZ() * z_scale).append("\"\n")
969 .append(in).append("links=\"")
971 if (null != hs_linked && 0 != hs_linked.size()) {
972 int ii = 0;
973 int len = hs_linked.size();
974 for (final Displayable d : hs_linked) {
975 data.append(d.getId());
976 if (ii != len-1) data.append(',');
977 ii++;
980 data.append("\"\n")
981 .append(indent).append("/>\n")
985 /** Returns a triple array, each containing a [2][n_points] array
986 * specifiying the x,y of each left control point, backbone point
987 * and right control point respectively. All of them are copies. */
988 public double[][][] getBezierArrays() {
989 //assumes the profile is a Bezier curve.
990 //put points and control points into PGpoint objects, as: LPRLPRLPR... (L = left control point, P = backbone point, R = right control point)
991 if (-1 == n_points) setupForDisplay(); // reload
992 final double[][][] b = new double[3][2][];
993 b[0][0] = Utils.copy(p_l[0], n_points);
994 b[0][1] = Utils.copy(p_l[1], n_points);
995 b[1][0] = Utils.copy(p[0], n_points);
996 b[1][1] = Utils.copy(p[1], n_points);
997 b[2][0] = Utils.copy(p_r[0], n_points);
998 b[2][1] = Utils.copy(p_r[1], n_points);
999 return b;
1002 public boolean isDeletable() {
1003 return 0 == n_points;
1006 /** Returns true if it's linked to at least one patch in the same Layer. Otherwise returns false. */
1007 public boolean isLinked() {
1008 if (null == hs_linked || hs_linked.isEmpty()) return false;
1009 for (final Displayable d : hs_linked) {
1010 if (d instanceof Patch && d.layer.equals(this.layer)) return true;
1012 return false;
1015 /** Returns false if the target_layer contains a profile that is directly linked to this profile. */
1016 public boolean canSendTo(Layer target_layer) {
1017 if (null == hs_linked || hs_linked.isEmpty()) return false;
1018 for (final Displayable d : hs_linked) {
1019 if (d instanceof Profile && d.layer.equals(target_layer)) return false;
1021 return true;
1024 protected double[][] getFirstPoint()
1026 if (0 == n_points) return null;
1027 return new double[][]{{p_l[0][0],p_l[1][0]},{p[0][0],p[1][0]},{p_r[0][0],p_r[1][0]}};
1030 protected double[][] getLastPoint()
1032 return new double[][]{{p_l[0][n_points-1],p_l[1][n_points-1]},{p[0][n_points-1],p[1][n_points-1]},{p_r[0][n_points-1],p_r[1][n_points-1]}};
1035 public boolean hasPoints() {
1036 return 0 != n_points;
1039 protected void setPoints(double[][] p_l, double[][] p, double[][] p_r) {
1040 this.p_l = p_l;
1041 this.p = p;
1042 this.p_r = p_r;
1043 this.n_points = p_l[0].length;
1044 this.generateInterpolatedPoints(0.05);
1047 public void setPoints(double[][] p_l, double[][] p, double[][] p_r, boolean update) {
1048 setPoints(p_l, p, p_r);
1049 calculateBoundingBox();
1050 if (update) {
1051 updateInDatabase("points");
1052 repaint(true);
1056 protected void addPointsAtBegin(double[][] new_p_l, double[][] new_p, double[][] new_p_r) {
1057 double[][] tmp_p_l = new double[2][p_l[0].length + new_p_l[0].length];
1058 double[][] tmp_p = new double[2][p[0].length + new_p[0].length];
1059 double[][] tmp_p_r = new double[2][p_r[0].length + new_p_r[0].length];
1060 int i = 0;
1061 for (; i < new_p_l[0].length; i++) {
1062 tmp_p_l[0][i] = new_p_l[0][i];
1063 tmp_p_l[1][i] = new_p_l[1][i];
1064 tmp_p[0][i] = new_p[0][i];
1065 tmp_p[1][i] = new_p[1][i];
1066 tmp_p_r[0][i] = new_p_r[0][i];
1067 tmp_p_r[1][i] = new_p_r[1][i];
1069 for (int j = 0; j < n_points; j++, i++) {
1070 tmp_p_l[0][i] = p_l[0][j];
1071 tmp_p_l[1][i] = p_l[1][j];
1072 tmp_p[0][i] = p[0][j];
1073 tmp_p[1][i] = p[1][j];
1074 tmp_p_r[0][i] = p_r[0][j];
1075 tmp_p_r[1][i] = p_r[1][j];
1077 this.n_points += new_p_l[0].length;
1079 p_l = tmp_p_l;
1080 p = tmp_p;
1081 p_r = tmp_p_r;
1082 this.generateInterpolatedPoints(0.05);
1085 protected void addPointsAtEnd(double[][] new_p_l, double[][] new_p, double[][] new_p_r) {
1086 double[][] tmp_p_l = new double[2][p_l[0].length + new_p_l[0].length];
1087 double[][] tmp_p = new double[2][p[0].length + new_p[0].length];
1088 double[][] tmp_p_r = new double[2][p_r[0].length + new_p_r[0].length];
1089 int i = 0;
1090 for (; i < n_points; i++) {
1091 tmp_p_l[0][i] = p_l[0][i];
1092 tmp_p_l[1][i] = p_l[1][i];
1093 tmp_p[0][i] = p[0][i];
1094 tmp_p[1][i] = p[1][i];
1095 tmp_p_r[0][i] = p_r[0][i];
1096 tmp_p_r[1][i] = p_r[1][i];
1099 for (int j = 0; j < new_p_l[0].length; i++,j++) {
1100 tmp_p_l[0][i] = new_p_l[0][j];
1101 tmp_p_l[1][i] = new_p_l[1][j];
1102 tmp_p[0][i] = new_p[0][j];
1103 tmp_p[1][i] = new_p[1][j];
1104 tmp_p_r[0][i] = new_p_r[0][j];
1105 tmp_p_r[1][i] = new_p_r[1][j];
1107 this.n_points += new_p_l[0].length;
1109 p_l = tmp_p_l;
1110 p = tmp_p;
1111 p_r = tmp_p_r;
1112 this.generateInterpolatedPoints(0.05);
1115 public int getNearestPointIndex(double x_p, double y_p) {
1116 int ret = -1;
1117 double minDist = Double.POSITIVE_INFINITY;
1118 for (int i = 0; i < this.n_points; i++) {
1119 double dx = this.p[0][i]-x_p;
1120 double dy = this.p[1][i]-y_p;
1121 double dist = dx*dx+dy*dy;
1122 if(dist < minDist)
1124 minDist = dist;
1125 ret = i;
1128 return ret;
1131 public void insertBetween(int startIndex, int endIndex, double[][] tmp_p_l, double[][] tmp_p, double[][] tmp_p_r){
1132 if(endIndex < startIndex)
1134 for (int i = 0; i < 2; i++) {
1135 for (int j = 0; j < tmp_p[0].length/2; j++) {
1136 double tmppl = tmp_p_l[i][j];
1137 double tmpp = tmp_p[i][j];
1138 double tmppr = tmp_p_r[i][j];
1140 tmp_p_r[i][j] = tmp_p_l[i][tmp_p_l[0].length -1- j];
1141 tmp_p[i][j] = tmp_p[i][tmp_p[0].length -1- j];
1142 tmp_p_l[i][j] = tmp_p_r[i][tmp_p_r[0].length -1- j];
1144 tmp_p_r[i][tmp_p_l[0].length -1- j]=tmppl;
1145 tmp_p[i][tmp_p[0].length -1- j]=tmpp;
1146 tmp_p_l[i][tmp_p_r[0].length -1- j]=tmppr;
1150 int tmp = startIndex;
1151 startIndex = endIndex;
1152 endIndex = tmp;
1156 double[][] beginning_p_l;
1157 double[][] beginning_p;
1158 double[][] beginning_p_r;
1160 double[][] ending_p_l;
1161 double[][] ending_p;
1162 double[][] ending_p_r;
1164 if(endIndex - startIndex < n_points + startIndex - endIndex || closed == false)
1166 beginning_p_l = new double [2][startIndex+1];
1167 beginning_p = new double [2][startIndex+1];
1168 beginning_p_r = new double [2][startIndex+1];
1170 ending_p_l = new double [2][n_points- endIndex];
1171 ending_p = new double [2][n_points- endIndex];
1172 ending_p_r = new double [2][n_points- endIndex];
1174 for(int i = 0 ; i <= startIndex ; i++)
1176 for (int j = 0; j < 2; j++) {
1177 beginning_p_l [j][i] = this.p_l[j][i];
1178 beginning_p [j][i] = this.p[j][i];
1179 beginning_p_r [j][i] = this.p_r[j][i];
1182 for(int i = endIndex ; i < this.n_points ; i++)
1184 for (int j = 0; j < 2; j++) {
1185 ending_p_l [j][i-endIndex] = this.p_l[j][i];
1186 ending_p [j][i-endIndex] = this.p[j][i];
1187 ending_p_r [j][i-endIndex] = this.p_r[j][i];
1190 System.out.println("1");
1192 else
1194 beginning_p_l = new double [2][endIndex-startIndex + 1];
1195 beginning_p = new double [2][ endIndex-startIndex + 1 ];
1196 beginning_p_r = new double [2][endIndex-startIndex + 1];
1198 ending_p_l = new double [2][0];
1199 ending_p = new double [2][0];
1200 ending_p_r = new double [2][0];
1202 for(int i = startIndex ; i <= endIndex ; i++)
1204 for (int j = 0; j < 2; j++) {
1205 beginning_p_r [j][endIndex - i] = this.p_l[j][i];
1206 beginning_p [j][endIndex - i] = this.p[j][i];
1207 beginning_p_l [j][endIndex - i] = this.p_r[j][i];
1211 System.out.println("2");
1217 double[][] new_p_l = new double[2][beginning_p_l[0].length + ending_p_l[0].length + tmp_p_l[0].length];
1218 double[][] new_p = new double[2][beginning_p[0].length + ending_p[0].length + tmp_p[0].length];
1219 double[][] new_p_r = new double[2][beginning_p_r[0].length + ending_p_r[0].length + tmp_p_r[0].length];
1221 for (int i = 0; i < beginning_p[0].length; i++) {
1222 for (int j = 0; j < 2; j++) {
1223 new_p_l[j][i] = beginning_p_l[j][i];
1224 new_p[j][i] = beginning_p[j][i];
1225 new_p_r[j][i] = beginning_p_r[j][i];
1228 for (int i = 0; i < tmp_p[0].length; i++) {
1229 for (int j = 0; j < 2; j++) {
1230 new_p_l[j][i+beginning_p[0].length] = tmp_p_l[j][i];
1231 new_p[j][i+beginning_p[0].length] = tmp_p[j][i];
1232 new_p_r[j][i+beginning_p[0].length] = tmp_p_r[j][i];
1235 for (int i = 0; i < ending_p[0].length; i++) {
1236 for (int j = 0; j < 2; j++) {
1237 new_p_l[j][i+beginning_p[0].length+tmp_p[0].length] = ending_p_l[j][i];
1238 new_p[j][i+beginning_p[0].length+tmp_p[0].length] = ending_p[j][i];
1239 new_p_r[j][i+beginning_p[0].length+tmp_p[0].length] = ending_p_r[j][i];
1242 this.n_points = new_p[0].length;
1243 this.p_l = new_p_l;
1244 this.p = new_p;
1245 this.p_r = new_p_r;
1246 this.calculateBoundingBox();
1247 this.generateInterpolatedPoints(0.05);
1250 public void printPoints() {
1251 System.out.println("#####\nw,h: " + width + "," + height);
1252 for (int i=0; i<n_points; i++) {
1253 System.out.println("x,y: " + p[0][i] + " , " + p[1][i]);
1255 System.out.println("\n");
1259 /** x,y is the cursor position in offscreen coordinates. */ // COPIED from the Pipe
1260 public void snapTo(int cx, int cy, int x_p, int y_p) { // WARNING: DisplayCanvas is locking at mouseDragged when the cursor is outside the DisplayCanvas Component, so this is useless or even harmful at the moment.
1261 if (-1 != index) {
1262 // #$#@$%#$%!!! TODO this doesn't work, although it *should*. The index_l and index_r work, and the mouseEntered coordinates are fine too. Plus it messes up the x,y position or something, for then on reload the pipe is streched or displaced (not clear).
1264 double dx = p_l[0][index] - p[0][index];
1265 double dy = p_l[1][index] - p[1][index];
1266 p_l[0][index] = cx + dx;
1267 p_l[1][index] = cy + dy;
1268 dx = p_r[0][index] - p[0][index];
1269 dy = p_r[1][index] - p[1][index];
1270 p_r[0][index] = cx + dx;
1271 p_r[1][index] = cy + dy;
1272 p[0][index] = cx;
1273 p[1][index] = cy;
1275 } else if (-1 != index_l) {
1276 p_l[0][index_l] = cx;
1277 p_l[1][index_l] = cy;
1278 } else if (-1 != index_r) {
1279 p_r[0][index_r] = cx;
1280 p_r[1][index_r] = cy;
1281 } else {
1282 // drag the whole pipe
1283 // CONCEPTUALLY WRONG, what happens when not dragging the pipe, on mouseEntered? Disaster!
1284 //drag(cx - x_p, cy - y_p);
1288 @Override
1289 public void exportXML(final StringBuilder sb_body, final String indent, final XMLOptions options) {
1290 sb_body.append(indent).append("<t2_profile\n");
1291 final String in = indent + "\t";
1292 super.exportXML(sb_body, in, options);
1293 if (-1 == n_points) setupForDisplay(); // reload
1294 final String[] RGB = Utils.getHexRGBColor(color);
1295 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;\"\n");
1296 if (n_points > 0) {
1297 sb_body.append(in).append("d=\"M");
1298 for (int i=0; i<n_points-1; i++) {
1299 sb_body.append(' ').append(p[0][i]).append(',').append(p[1][i])
1300 .append(" C ").append(p_r[0][i]).append(',').append(p_r[1][i])
1301 .append(' ').append(p_l[0][i+1]).append(',').append(p_l[1][i+1])
1304 sb_body.append(' ').append(p[0][n_points-1]).append(',').append(p[1][n_points-1]);
1305 if (closed) {
1306 sb_body.append(" C ").append(p_r[0][n_points-1]).append(',').append(p_r[1][n_points-1])
1307 .append(' ').append(p_l[0][0]).append(',').append(p_l[1][0])
1308 .append(' ').append(p[0][0]).append(',').append(p[1][0])
1309 .append(" z")
1312 sb_body.append("\"\n");
1314 sb_body.append(indent).append(">\n");
1315 super.restXML(sb_body, in, options);
1316 sb_body.append(indent).append("</t2_profile>\n");
1319 static public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
1320 final String type = "t2_profile";
1321 if (hs.contains(type)) return;
1322 hs.add(type);
1323 sb_header.append(indent).append("<!ELEMENT t2_profile (").append(Displayable.commonDTDChildren()).append(")>\n");
1324 Displayable.exportDTD(type, sb_header, hs, indent);
1325 sb_header.append(indent).append(TAG_ATTR1).append(type).append(" d").append(TAG_ATTR2)
1329 /** Returns the interpolated points as a VectorString2D, calibrated.
1330 * Returns null if there aren't any points. */
1331 public VectorString2D getPerimeter2D() {
1332 return getPerimeter2D(layer.getParent().getCalibration());
1334 private VectorString2D getPerimeter2D(final Calibration cal) {
1335 if (-1 == n_points) setupForDisplay();
1336 if (0 == n_points) return null;
1337 if (0 == p_i[0].length) generateInterpolatedPoints(0.05);
1338 double[][] pi = transformPoints(p_i);
1339 VectorString2D sv = null;
1340 try {
1341 sv = new VectorString2D(pi[0], pi[1], this.layer.getZ(), this.closed);
1342 } catch (Exception e) {
1343 IJError.print(e);
1345 if (null != cal) sv.calibrate(cal);
1346 return sv;
1349 public void keyPressed(KeyEvent ke) {
1350 super.keyPressed(ke);
1351 if (ke.isConsumed()) return;
1352 int key_code = ke.getKeyCode();
1353 Rectangle box = null;
1354 switch(key_code) {
1355 case KeyEvent.VK_X: // remove all points
1356 if (0 == ke.getModifiers() && (ProjectToolbar.getToolId() == ProjectToolbar.PEN || ProjectToolbar.getToolId() == ProjectToolbar.PENCIL)) {
1357 box = getBoundingBox(box);
1358 n_points = 0;
1359 this.p_i = new double[2][0];
1360 calculateBoundingBox(true);
1361 ke.consume();
1362 if (closed) toggleClosed();
1363 updateInDatabase("points");
1365 break;
1366 case KeyEvent.VK_C: // toggle close with shift+c
1367 if (0 == (ke.getModifiers() ^ java.awt.Event.SHIFT_MASK)) {
1368 //preconditions: at least 2 points!
1369 if (n_points > 1) {
1370 toggleClosed();
1371 generateInterpolatedPoints(0.05);
1372 ke.consume();
1375 break;
1377 if (ke.isConsumed()) {
1378 Display.repaint(this.layer, box, 5);
1382 public void setColor(Color c) {
1383 // propagate to all linked profiles within the same profile_list
1384 setColor(c, new HashSet<Profile>());
1387 /** Exploits the fact that Profile instances among the directly linked as returned by getLinked(Profile.class) will be members of the same profile_list. */
1388 private void setColor(Color c, HashSet<Profile> hs_done) {
1389 if (hs_done.contains(this)) return;
1390 hs_done.add(this);
1391 super.setColor(c);
1392 HashSet<Displayable> hs = getLinked(Profile.class);
1393 if (null != hs) {
1394 for (Iterator<Displayable> it = hs.iterator(); it.hasNext(); ) {
1395 Profile p = (Profile)it.next();
1396 p.setColor(c, hs_done);
1401 /** Performs a deep copy of this object, unlocked and visible. */
1402 public Displayable clone(final Project pr, final boolean copy_id) {
1403 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
1404 final Profile copy = new Profile(pr, nid, null != title ? title.toString() : null, width, height, alpha, this.visible, new Color(color.getRed(), color.getGreen(), color.getBlue()), closed, this.locked, (AffineTransform)this.at.clone());
1405 // The data:
1406 if (-1 == n_points) setupForDisplay(); // load data
1407 copy.n_points = n_points;
1408 copy.p = new double[][]{(double[])this.p[0].clone(), (double[])this.p[1].clone()};
1409 copy.p_l = new double[][]{(double[])this.p_l[0].clone(), (double[])this.p_l[1].clone()};
1410 copy.p_r = new double[][]{(double[])this.p_r[0].clone(), (double[])this.p_r[1].clone()};
1411 copy.p_i = new double[][]{(double[])this.p_i[0].clone(), (double[])this.p_i[1].clone()};
1412 // add
1413 copy.addToDatabase();
1415 return copy;
1418 private Object[] getTransformedData() {
1419 final double[][] p = transformPoints(this.p);
1420 final double[][] p_l = transformPoints(this.p_l);
1421 final double[][] p_r = transformPoints(this.p_r);
1422 final double[][] p_i = transformPoints(this.p_i);
1423 return new Object[]{p, p_l, p_r, p_i};
1426 /** Takes a profile_list, scans for its Profile children, makes sublists of continuous profiles (if they happen to be branched), and then creates triangles for them using weighted vector strings. */
1427 static public List<Point3f> generateTriangles(final ProjectThing pt, final double scale) {
1428 if (!pt.getType().equals("profile_list")) {
1429 Utils.log2("Profile: ignoring unhandable ProjectThing type.");
1430 return null;
1432 ArrayList<ProjectThing> al = pt.getChildren(); // should be sorted by Z already
1433 if (al.size() < 2) {
1434 Utils.log("profile_list " + pt + " has less than two profiles: can't render in 3D.");
1435 return null;
1437 // collect all Profile
1438 final HashSet<Profile> hs = new HashSet<Profile>();
1439 for (final ProjectThing child : al) {
1440 Object ob = child.getObject();
1441 if (ob instanceof Profile) {
1442 hs.add((Profile)ob);
1443 } else {
1444 Utils.log2("Render: skipping non Profile class child");
1447 // Create sublists of profiles, following the chain of links.
1448 final Profile[] p = new Profile[hs.size()];
1449 hs.toArray(p);
1450 // find if at least one is visible
1451 boolean hidden = true;
1452 for (int i=0; i<p.length; i++) {
1453 if (p[i].visible) {
1454 hidden = false;
1455 break;
1457 if (null == p[i] || 0 == p[i].n_points) {
1458 Utils.log("Cannot generate triangle mesh: empty profile " + p[i] + (null != p[i] ? " at layer " + p[i].getLayer() : ""));
1459 return null;
1462 if (hidden) return null;
1463 // collect starts and ends
1464 final HashSet<Profile> hs_bases = new HashSet<Profile>();
1465 final HashSet<Profile> hs_done = new HashSet<Profile>();
1466 final List<Point3f> triangles = new ArrayList<Point3f>();
1467 do {
1468 Profile base = null;
1469 // choose among existing bases
1470 if (hs_bases.size() > 0) {
1471 base = hs_bases.iterator().next();
1472 } else {
1473 // find a new base, simply by taking the lowest Z or remaining profiles
1474 double min_z = Double.MAX_VALUE;
1475 for (int i=0; i<p.length; i++) {
1476 if (hs_done.contains(p[i])) continue;
1477 double z = p[i].getLayer().getZ();
1478 if (z < min_z) {
1479 min_z = z;
1480 base = p[i];
1483 // add base
1484 if (null != base) hs_bases.add(base);
1486 if (null == base) {
1487 Utils.log2("No more bases.");
1488 break;
1490 // crawl list to get a sequence of profiles in increasing or decreasing Z order, but not mixed z trends
1491 final ArrayList<Profile> al_profiles = new ArrayList<Profile>();
1492 //Utils.log2("Calling accumulate for base " + base);
1493 al_profiles.add(base);
1494 final Profile last = accumulate(hs_done, al_profiles, base, 0);
1495 // if the trend was not empty, add it
1496 if (last != base) {
1497 // count as done
1498 hs_done.addAll(al_profiles);
1499 // add new possible base (which may have only 2 links if it was from a broken Z trend)
1500 hs_bases.add(last);
1501 // create 3D object from base to base
1502 final Profile[] profiles = new Profile[al_profiles.size()];
1503 al_profiles.toArray(profiles);
1504 List<Point3f> tri = makeTriangles(profiles, scale);
1505 if (null != tri) triangles.addAll(tri);
1506 } else {
1507 // remove base
1508 hs_bases.remove(base);
1510 } while (0 != hs_bases.size());
1512 return triangles;
1515 /** Recursive; returns the last added profile. */
1516 static private Profile accumulate(final HashSet<Profile> hs_done, final ArrayList<Profile> al, final Profile step, int z_trend) {
1517 final HashSet<Displayable> hs_linked = step.getLinked(Profile.class);
1518 if (al.size() > 1 && hs_linked.size() > 2) {
1519 // base found
1520 return step;
1522 double step_z = step.getLayer().getZ();
1523 Profile next_step = null;
1524 boolean started = false;
1525 for (Iterator<Displayable> it = hs_linked.iterator(); it.hasNext(); ) {
1526 Object ob = it.next();
1527 // loop only one cycle, to move only in one direction
1528 if (al.contains(ob) || started || hs_done.contains(ob)) continue;
1529 started = true;
1530 next_step = (Profile)ob;
1531 double next_z = next_step.getLayer().getZ();
1532 if (0 == z_trend) {
1533 // define trend
1534 if (next_z > step_z) {
1535 z_trend = 1;
1536 } else {
1537 z_trend = -1;
1539 // add!
1540 al.add(next_step);
1541 } else {
1542 // if the z trend is broken, finish
1543 if ( (next_z > step_z && 1 == z_trend)
1544 || (next_z < step_z && -1 == z_trend) ) {
1545 // z trend continues
1546 al.add(next_step);
1547 } else {
1548 // z trend broken
1549 next_step = null;
1553 Profile last = step;
1554 if (null != next_step) {
1555 hs_done.add(next_step);
1556 last = accumulate(hs_done, al, next_step, z_trend);
1558 return last;
1561 /** Make a mesh as a calibrated list of 3D triangles.*/
1562 static private List<Point3f> makeTriangles(final Profile[] p, final double scale) {
1563 try {
1564 final VectorString2D[] sv = new VectorString2D[p.length];
1565 boolean closed = true; // dummy initialization
1566 final Calibration cal = p[0].getLayerSet().getCalibrationCopy();
1567 cal.pixelWidth *= scale;
1568 cal.pixelHeight *= scale;
1569 for (int i=0; i<p.length; i++) {
1570 if (0 == p[i].n_points) continue;
1571 if (0 == i) closed = p[i].closed;
1572 else if (p[i].closed != closed) {
1573 Utils.log2("All profiles should be either open or closed, not mixed.");
1574 return null;
1576 sv[i] = p[i].getPerimeter2D(cal);
1578 return SkinMaker.generateTriangles(sv, -1, -1, closed);
1579 } catch (Exception e) {
1580 IJError.print(e);
1582 return null;
1585 /** Does nothing. */
1586 @Override
1587 public boolean softRemove() {
1588 return true;
1591 protected boolean remove2(boolean check) {
1592 return project.getProjectTree().remove(check, project.findProjectThing(this), null); // will call remove(check) here
1595 /** Calibrated for pixel width only (that is, it assumes pixel aspect ratio 1:1), in units as specified at getLayerSet().getCalibration().getUnit() */
1596 public double computeLength() {
1597 if (-1 == n_points || 0 == this.p_i[0].length) setupForDisplay();
1598 if (this.p_i[0].length < 2) return 0;
1599 final double[][] p_i = transformPoints(this.p_i);
1600 double len = 0;
1601 for (int i=1; i<p_i[0].length; i++) {
1602 len += Math.sqrt(Math.pow(p_i[0][i] - p_i[0][i-1], 2) + Math.pow(p_i[1][i] - p_i[1][i-1], 2));
1604 if (closed) {
1605 int last = p[0].length -1;
1606 len += Math.sqrt(Math.pow(p_i[0][last] - p_i[0][0], 2) + Math.pow(p_i[1][last] - p_i[1][0], 2));
1608 // to calibrate for pixelWidth and pixelHeight, I'd have to multiply each x,y values above separately
1609 return len * getLayerSet().getCalibration().pixelWidth;
1612 /** Calibrated, in units as specified at getLayerSet().getCalibration().getUnit() -- returns zero if this profile is not closed. */
1613 public double computeArea() {
1614 if (-1 == n_points) setupForDisplay();
1615 if (n_points < 2) return 0;
1616 if (!closed) return 0;
1617 if (0 == p_i[0].length) generateInterpolatedPoints(0.05);
1618 Calibration cal = getLayerSet().getCalibration();
1619 return M.measureArea(new Area(getPerimeter()), getProject().getLoader()) * cal.pixelWidth * cal.pixelHeight;
1622 /** Measures the calibrated length, the lateral surface as the length times the layer thickness, and the volume (if closed) as the area times the layer thickness. */
1623 @Override
1624 public ResultsTable measure(ResultsTable rt) {
1625 if (null == rt) rt = Utils.createResultsTable("Profile results", new String[]{"id", "length", "side surface: length x thickness", "volume: area x thickness", "name-id"});
1626 if (-1 == n_points) setupForDisplay();
1627 if (n_points < 2) return null;
1628 if (0 == p_i[0].length) generateInterpolatedPoints(0.05);
1629 final Calibration cal = getLayerSet().getCalibration();
1630 // computeLength returns a calibrated length, so only calibrate the layer thickness:
1631 final double len = computeLength();
1632 final double surface_flat = len * layer.getThickness() * cal.pixelWidth;
1633 rt.incrementCounter();
1634 rt.addLabel("units", cal.getUnit());
1635 rt.addValue(0, id);
1636 rt.addValue(1, len);
1637 rt.addValue(2, surface_flat);
1638 final double volume = closed ? computeArea() * layer.getThickness() * cal.pixelWidth : 0;
1639 rt.addValue(3, volume);
1640 rt.addValue(4, getNameId());
1641 return rt;
1644 /** Assumes Z-coord sorted list of profiles, as stored in a "profile_list" ProjectThing type. . */
1645 static public ResultsTable measure(final Profile[] profiles, ResultsTable rt, final long profile_list_id) {
1646 Utils.log2("profiles.length" + profiles.length);
1647 if (null == profiles || 0 == profiles.length) return null;
1648 if (1 == profiles.length) {
1649 // don't measure if there is only one
1650 return rt;
1652 for (final Profile p : profiles) {
1653 if (null == p || 0 == p.n_points) {
1654 Utils.log("Cannot measure: empty profile " + p + (null != p ? " at layer " + p.getLayer() : ""));
1655 return rt;
1658 if (null == rt) rt = Utils.createResultsTable("Profile list results", new String[]{"id", "interpolated surface", "surface: sum of length x thickness", "volume", "name-id"});
1659 Calibration cal = profiles[0].getLayerSet().getCalibration();
1660 // else, interpolate skin and measure each triangle
1661 List<Point3f> tri = makeTriangles(profiles, 1.0); // already calibrated
1662 final int n_tri = tri.size();
1663 if (0 != n_tri % 3) {
1664 Utils.log("Profile.measure error: triangle verts list not a multiple of 3 for profile list id " + profile_list_id);
1665 return rt;
1667 // Surface: calibrated sum of the area of all triangles in the mesh.
1668 double surface = 0;
1669 for (int i=2; i<n_tri; i+=3) {
1670 surface += M.measureArea(tri.get(i-2), tri.get(i-1), tri.get(i));
1672 // add capping ends
1673 double area_first = profiles[0].computeArea();
1674 double area_last = profiles[profiles.length-1].computeArea();
1675 if (profiles[0].closed) surface += area_first;
1676 if (profiles[profiles.length-1].closed) surface += area_last;
1678 // Surface flat: sum of the perimeter lengths times the layer thickness
1679 double surface_flat = 0;
1680 for (int i=0; i<profiles.length; i++) {
1681 if (0 == profiles[i].p_i[0].length) profiles[i].generateInterpolatedPoints(0.05);
1682 surface_flat += profiles[i].computeLength() * profiles[i].layer.getThickness() * cal.pixelWidth;
1685 // Volume: area times layer thickness
1686 double volume = area_first * profiles[0].layer.getThickness();
1687 for (int i=1; i<profiles.length-1; i++) {
1688 volume += profiles[i].computeArea() * profiles[i].layer.getThickness();
1690 volume += area_last * profiles[profiles.length-1].layer.getThickness();
1692 // calibrate volume: the Z is still in pixels
1693 volume *= cal.pixelWidth;
1695 rt.incrementCounter();
1696 rt.addLabel("units", cal.getUnit());
1697 rt.addValue(0, profile_list_id);
1698 rt.addValue(1, surface);
1699 rt.addValue(2, surface_flat);
1700 rt.addValue(3, volume);
1701 double nameid = 0;
1702 try {
1703 nameid = Double.parseDouble(profiles[0].project.findProjectThing(profiles[0]).getParent().getTitle());
1704 } catch (NumberFormatException nfe) {}
1705 rt.addValue(4, nameid);
1706 return rt;
1709 @Override
1710 final Class<?> getInternalDataPackageClass() {
1711 return DPProfile.class;
1714 @Override
1715 synchronized Object getDataPackage() {
1716 return new DPProfile(this);
1719 static private final class DPProfile extends Displayable.DataPackage {
1720 final double[][] p, p_l, p_r, p_i;
1721 final boolean closed;
1723 DPProfile(final Profile profile) {
1724 super(profile);
1725 // store copies of all arrays
1726 this.p = new double[][]{Utils.copy(profile.p[0], profile.n_points), Utils.copy(profile.p[1], profile.n_points)};
1727 this.p_r = new double[][]{Utils.copy(profile.p_r[0], profile.n_points), Utils.copy(profile.p_r[1], profile.n_points)};
1728 this.p_l = new double[][]{Utils.copy(profile.p_l[0], profile.n_points), Utils.copy(profile.p_l[1], profile.n_points)};
1729 this.p_i = new double[][]{Utils.copy(profile.p_i[0], profile.p_i[0].length), Utils.copy(profile.p_i[1], profile.p_i[0].length)};
1730 this.closed = profile.closed;
1732 @Override
1733 final boolean to2(final Displayable d) {
1734 super.to1(d);
1735 final Profile profile = (Profile)d;
1736 final int len = p[0].length; // == n_points, since it was cropped on copy
1737 profile.p = new double[][]{Utils.copy(p[0], len), Utils.copy(p[1], len)};
1738 profile.n_points = p[0].length;
1739 profile.p_r = new double[][]{Utils.copy(p_r[0], len), Utils.copy(p_r[1], len)};
1740 profile.p_l = new double[][]{Utils.copy(p_l[0], len), Utils.copy(p_l[1], len)};
1741 profile.p_i = new double[][]{Utils.copy(p_i[0], p_i[0].length), Utils.copy(p_i[1], p_i[1].length)};
1742 profile.closed = closed;
1743 return true;
1747 // It's such a pitty that this code is almost identical to that of the Pipe, and it can't be abstracted effectively any further.
1748 synchronized public boolean apply(final Layer la, final Area roi, final mpicbg.models.CoordinateTransform ict) throws Exception {
1749 if (this.layer != la) return true;
1750 float[] fp = null;
1751 mpicbg.models.CoordinateTransform chain = null;
1752 Area localroi = null;
1753 AffineTransform inverse = null;
1754 for (int i=0; i<n_points; i++) {
1755 if (null == localroi) {
1756 inverse = this.at.createInverse();
1757 localroi = roi.createTransformedArea(inverse);
1759 if (localroi.contains(p[0][i], p[1][i])) {
1760 if (null == chain) {
1761 chain = M.wrap(this.at, ict, inverse);
1762 fp = new float[2];
1764 // The point and its two associated control points:
1765 M.apply(chain, p, i, fp);
1766 M.apply(chain, p_l, i, fp);
1767 M.apply(chain, p_r, i, fp);
1770 if (null != chain) {
1771 generateInterpolatedPoints(0.05);
1772 calculateBoundingBox(true);
1774 return true;
1776 public boolean apply(final VectorDataTransform vdt) throws Exception {
1777 if (vdt.layer != this.layer) return false;
1778 final float[] fp = new float[2];
1779 final VectorDataTransform vlocal = vdt.makeLocalTo(this);
1780 for (int i=0; i<n_points; i++) {
1781 for (final VectorDataTransform.ROITransform rt : vlocal.transforms) {
1782 if (rt.roi.contains(p[0][i], p[1][i])) {
1783 // The point and its two associated control points:
1784 M.apply(rt.ct, p, i, fp);
1785 M.apply(rt.ct, p_l, i, fp);
1786 M.apply(rt.ct, p_r, i, fp);
1787 break;
1791 generateInterpolatedPoints(0.05);
1792 calculateBoundingBox(true);
1793 return true;
1796 @Override
1797 synchronized public boolean isRoughlyInside(final Layer layer, final Rectangle r) {
1798 if (this.layer != layer) return false;
1799 try {
1800 final Rectangle box = this.at.createInverse().createTransformedShape(r).getBounds();
1801 for (int i=0; i<n_points; i++) {
1802 if (box.contains(p[0][i], p[1][i])) return true;
1804 } catch (NoninvertibleTransformException e) {
1805 IJError.print(e);
1807 return false;