Fixes to homogenizeContrast
[trakem2.git] / ini / trakem2 / display / Profile.java
blobe11fe65bfaf21ca8b1033e8a306f4274e2f6662a
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 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;
29 import ini.trakem2.Project;
30 import ini.trakem2.persistence.DBObject;
31 import ini.trakem2.persistence.Loader;
32 import ini.trakem2.utils.ProjectToolbar;
33 import ini.trakem2.utils.Utils;
34 import ini.trakem2.utils.M;
35 import ini.trakem2.utils.IJError;
36 import ini.trakem2.render3d.Perimeter2D;
37 import ini.trakem2.display.Display3D;
38 import ini.trakem2.tree.ProjectThing;
39 import ini.trakem2.tree.Thing;
40 import ini.trakem2.tree.ProjectThing;
41 import ini.trakem2.vector.VectorString2D;
42 import ini.trakem2.vector.SkinMaker;
44 import java.awt.Color;
45 import java.awt.Rectangle;
46 import java.awt.Polygon;
47 import java.awt.event.MouseEvent;
48 import java.awt.event.KeyEvent;
49 import java.awt.geom.Area;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Map;
53 import java.util.HashMap;
54 import java.util.HashSet;
55 import java.util.Iterator;
56 import java.util.List;
57 import java.awt.geom.AffineTransform;
58 import java.awt.geom.Point2D;
59 import java.awt.Graphics;
60 import java.awt.Graphics2D;
61 import java.awt.Composite;
62 import java.awt.AlphaComposite;
64 import javax.vecmath.Point3f;
67 /** 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.
68 * TODO - label not implemented yet.
69 * - can't paint segments in different colors yet
70 * - the whole curve is updated when only a particular set of points needs readjustment
71 * - also, points are smooth, and options should be given to make them non-smooth.
73 public class Profile extends Displayable {
75 /**The number of points.*/
76 protected int n_points;
77 /**The array of clicked points.*/
78 protected double[][] p;
79 /**The array of left control points, one for each clicked point.*/
80 protected double[][] p_l;
81 /**The array of right control points, one for each clicked point.*/
82 protected double[][] p_r;
83 /**The array of interpolated points generated from p, p_l and p_r.*/
84 protected double[][] p_i;
85 /**Paint/behave as open or closed curve.*/
86 protected boolean closed = false;
88 /** A new user-requested Profile.*/
89 public Profile(Project project, String title, double x, double y) {
90 super(project, title, x, y);
91 n_points = 0;
92 p = new double[2][5];
93 p_l = new double[2][5];
94 p_r = new double[2][5];
95 p_i = new double[2][0];
96 addToDatabase();
99 /**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.*/
100 public Profile(Project project, String title, double x, double y, Point2D.Double[] points) {
101 super(project, title, x, y);
102 //setup arrays
103 int size = (points.length / 3) + 1;
104 p = new double[2][size];
105 p_l =new double[2][size];
106 p_r =new double[2][size];
107 //first point:
108 p[0][0] = points[0].x;
109 p[1][0] = points[0].y;
110 p_l[0][0] = p[0][0];
111 p_l[1][0] = p[1][0];
112 p_r[0][0] = points[1].x;
113 p_r[1][0] = points[1].y;
114 n_points++;
115 //all middle points:
116 for (int j=1, i=3; i<points.length-3; i+=3, j++) {
117 p[0][j] = points[i].x;
118 p[1][j] = points[i].y;
119 p_l[0][j] = points[i-1].x;
120 p_l[1][j] = points[i-1].y;
121 if (null == points[i+1]) {
122 Utils.log("BezierProfile: points[" + i + " + 1] is null !");
124 p_r[0][j] = points[i+1].x;
125 p_r[1][j] = points[i+1].y;
126 n_points++;
128 //last point:
129 int last = points.length-1;
130 p[0][n_points] = points[last].x;
131 p[1][n_points] = points[last].y;
132 p_l[0][n_points] = points[last-1].x;
133 p_l[1][n_points] = points[last-1].y;
134 p_r[0][n_points] = p[0][n_points];
135 p_r[1][n_points] = p[1][n_points];
136 n_points++;
138 calculateBoundingBox();
140 //add to database
141 addToDatabase();
142 updateInDatabase("points");
145 /**Construct a Bezier Profile from the database.*/
146 public Profile(Project project, long id, String title, float alpha, boolean visible, Color color, double[][][] bezarr, boolean closed, boolean locked, AffineTransform at) {
147 super(project, id, title, locked, at, 0, 0);
148 this.visible = visible;
149 this.alpha = alpha;
150 this.color = color;
151 this.closed = closed;
152 //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!)
153 //// points are fields x,y in PGpoint.
154 p_l = bezarr[0];
155 p = bezarr[1];
156 p_r = bezarr[2];
157 n_points = p[0].length;
159 //calculate width and height
160 calculateBoundingBox(false);
163 /** Construct a Bezier Profile from the database, but the points will be loaded later, when actually needed, by calling setupForDisplay(). */
164 public Profile(Project project, long id, String title, double width, double height, float alpha, boolean visible, Color color, boolean closed, boolean locked, AffineTransform at) {
165 super(project, id, title, locked, at, width, height);
166 this.visible = visible;
167 this.alpha = alpha;
168 this.color = color;
169 this.closed = closed;
170 this.n_points = -1; //used as a flag to signal "I have points, but unloaded"
173 /** Construct a Bezier Profile from an XML entry. */
174 public Profile(Project project, long id, HashMap ht, HashMap ht_links) {
175 super(project, id, ht, ht_links);
176 // parse data
177 for (Iterator it = ht.entrySet().iterator(); it.hasNext(); ) {
178 Map.Entry entry = (Map.Entry)it.next();
179 String key = (String)entry.getKey();
180 String data = (String)entry.getValue();
181 if (key.equals("d")) {
182 // parse the SVG points data
183 ArrayList al_p = new ArrayList();
184 ArrayList al_p_r = new ArrayList();
185 ArrayList al_p_l = new ArrayList();// needs shifting, inserting one point at the beginning if not closed.
186 // 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]
187 // first point:
188 int i_start = data.indexOf('M');
189 int i_end = data.indexOf('C');
190 String point = data.substring(i_start+1, i_end).trim();
191 al_p.add(point);
192 boolean go = true;
193 while (go) {
194 i_start = i_end;
195 i_end = data.indexOf('C', i_end+1);
196 if (-1 == i_end) {
197 i_end = data.length() -1;
198 go = false;
200 String txt = data.substring(i_start+1, i_end).trim();
201 // eliminate double spaces
202 while (-1 != txt.indexOf(" ")) {
203 txt = txt.replaceAll(" ", " ");
205 // reduce ", " and " ," to ","
206 txt = txt.replaceAll(" ,", ",");
207 txt = txt.replaceAll(", ", ",");
208 // cut by spaces
209 String[] points = txt.split(" ");
210 // debug:
211 //Utils.log("Profile init: txt=__" + txt + "__ points.length=" + points.length);
212 if (3 == points.length) {
213 al_p_r.add(points[0]);
214 al_p_l.add(points[1]);
215 al_p.add(points[2]);
216 } else {
217 // error
218 Utils.log("Profile constructor from XML: error at parsing points.");
220 // example: C 34.5,45.6 45.7,23.0 34.8, 78.0 C ..
222 this.closed = (-1 != data.lastIndexOf('z')); // 'z' must be lowercase to comply with SVG style
223 if (this.closed) {
224 // prepend last left control point and delete from the end
225 al_p_l.add(0, al_p_l.remove(al_p_l.size() -1));
226 // remove last point (duplicated in this SVG format)
227 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.
228 } else {
229 // prepend a left control point equal to the first point
230 al_p_l.add(0, al_p.get(0)); // no need to clone, String is final
231 // and append a right control point equal to the last point
232 al_p_r.add(al_p.get(al_p.size() -1));
234 // sanity check:
235 if (!(al_p.size() == al_p_l.size() && al_p_l.size() == al_p_r.size())) {
236 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());
238 // Now parse the points
239 this.n_points = al_p.size();
240 this.p = new double[2][n_points];
241 this.p_l = new double[2][n_points];
242 this.p_r = new double[2][n_points];
243 for (int i=0; i<n_points; i++) {
244 String[] sp = ((String)al_p.get(i)).split(",");
245 p[0][i] = Double.parseDouble(sp[0]);
246 p[1][i] = Double.parseDouble(sp[1]);
247 sp = ((String)al_p_l.get(i)).split(",");
248 p_l[0][i] = Double.parseDouble(sp[0]);
249 p_l[1][i] = Double.parseDouble(sp[1]);
250 sp = ((String)al_p_r.get(i)).split(",");
251 p_r[0][i] = Double.parseDouble(sp[0]);
252 p_r[1][i] = Double.parseDouble(sp[1]);
254 this.p_i = new double[2][0]; // empty
255 generateInterpolatedPoints(0.05);
260 /** A constructor for cloning purposes. */
261 private Profile(Project project, String title, double x, double y, double width, double height, float alpha, Color color, int n_points, double[][] p, double[][] p_r, double[][] p_l, double[][] p_i, boolean closed) {
262 super(project, title, x, y);
263 this.width = width;
264 this.height = height;
265 this.alpha = alpha;
266 this.color = color;
267 this.n_points = n_points;
268 this.p = p;
269 this.p_r = p_r;
270 this.p_l = p_l;
271 this.p_i = p_i;
272 this.closed = closed;
273 addToDatabase();
274 updateInDatabase("all");
277 /**Increase the size of the arrays by 5 points.*/
278 protected void enlargeArrays() {
279 //catch length
280 int length = p[0].length;
281 //make copies
282 double[][] p_copy = new double[2][length + 5];
283 double[][] p_l_copy = new double[2][length + 5];
284 double[][] p_r_copy = new double[2][length + 5];
285 //copy values
286 System.arraycopy(p[0], 0, p_copy[0], 0, length);
287 System.arraycopy(p[1], 0, p_copy[1], 0, length);
288 System.arraycopy(p_l[0], 0, p_l_copy[0], 0, length);
289 System.arraycopy(p_l[1], 0, p_l_copy[1], 0, length);
290 System.arraycopy(p_r[0], 0, p_r_copy[0], 0, length);
291 System.arraycopy(p_r[1], 0, p_r_copy[1], 0, length);
292 //assign them
293 this.p = p_copy;
294 this.p_l = p_l_copy;
295 this.p_r = p_r_copy;
298 public boolean isClosed() {
299 return this.closed;
302 /**Find a point in an array 'a', with a precision dependent on the magnification. */
303 protected int findPoint(double[][] a, double x_p, double y_p, double magnification) {
304 int index = -1;
305 // make parameters local
306 double d = (10.0D / magnification);
307 if (d < 2) d = 2;
308 for (int i=0; i<n_points; i++) {
309 if ((Math.abs(x_p - a[0][i]) + Math.abs(y_p - a[1][i])) <= d) {
310 index = i;
313 return index;
316 /**Remove a point from the bezier backbone and its two associated control points.*/
317 protected void removePoint(int index) {
318 // check preconditions:
319 if (index < 0) {
320 return;
321 } else if (n_points - 1 == index) {
322 //last point out
323 n_points--;
324 } else {
325 //one point out (but not the last)
326 n_points--;
327 // shift all points after 'index' one position to the left:
328 for (int i=index; i<n_points; i++) {
329 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.
330 p[1][i] = p[1][i+1];
331 p_l[0][i] = p_l[0][i+1];
332 p_l[1][i] = p_l[1][i+1];
333 p_r[0][i] = p_r[0][i+1];
334 p_r[1][i] = p_r[1][i+1];
337 // open the curve if necessary
338 if (closed && n_points < 2) {
339 closed = false;
340 updateInDatabase("closed");
342 //update in database
343 updateInDatabase("points");
346 /**Calculate distance from one point to another.*/
347 protected double distance(double x1, double y1, double x2, double y2) {
348 return Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
351 /**Move backbone point.*/
352 protected void dragPoint(int index, int dx, int dy) {
353 p[0][index] += dx;
354 p[1][index] += dy;
355 p_l[0][index] += dx;
356 p_l[1][index] += dy;
357 p_r[0][index] += dx;
358 p_r[1][index] += dy;
361 /**Set the control points to the same value as the backbone point which they control.*/
362 protected void resetControlPoints(int index) {
363 p_l[0][index] = p[0][index];
364 p_l[1][index] = p[1][index];
365 p_r[0][index] = p[0][index];
366 p_r[1][index] = p[1][index];
369 /**Drag a control point and adjust the other, dependent one, in a symmetric way or not.*/
370 protected void dragControlPoint(int index, double x_d, double y_d, double[][] p_dragged, double[][] p_adjusted, boolean symmetric) {
371 //measure hypothenusa: from p to p control
372 double hypothenusa;
373 if (symmetric) {
374 //make both points be dragged in parallel, the same distance
375 hypothenusa = distance(p[0][index], p[1][index], p_dragged[0][index], p_dragged[1][index]);
376 } else {
377 //make each point be dragged with its own distance
378 hypothenusa = distance(p[0][index], p[1][index], p_adjusted[0][index], p_adjusted[1][index]);
380 //measure angle: use the point being dragged
381 double angle = Math.atan2(p_dragged[0][index] - p[0][index], p_dragged[1][index] - p[1][index]) + Math.PI;
382 //apply
383 p_dragged[0][index] = x_d;
384 p_dragged[1][index] = y_d;
385 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
386 p_adjusted[1][index] = p[1][index] + hypothenusa * Math.cos(angle);
389 /**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. */
390 protected int addPoint(double x_p, double y_p, double magnification, double bezier_finess) {
391 //lookup closest interpolated point and then get the closest clicked point to it
392 int index = findClosestPoint(x_p, y_p, magnification, bezier_finess); // x_p, y_p are already local.
393 if (closed && -1 == index) {
394 return -1;
396 //check array size
397 if (p[0].length == n_points) {
398 enlargeArrays();
400 //decide:
401 if (0 == n_points || 1 == n_points || -1 == index || index + 1 == n_points) {
402 //append at the end
403 p[0][n_points] = p_l[0][n_points] = p_r[0][n_points] = x_p;
404 p[1][n_points] = p_l[1][n_points] = p_r[1][n_points] = y_p;
405 index = n_points;
406 } else {
407 //insert at index:
408 index++; //so it is added after the closest point;
409 // 1 - copy second half of array
410 int sh_length = n_points -index;
411 double[][] p_copy = new double[2][sh_length];
412 double[][] p_l_copy = new double[2][sh_length];
413 double[][] p_r_copy = new double[2][sh_length];
414 System.arraycopy(p[0], index, p_copy[0], 0, sh_length);
415 System.arraycopy(p[1], index, p_copy[1], 0, sh_length);
416 System.arraycopy(p_l[0], index, p_l_copy[0], 0, sh_length);
417 System.arraycopy(p_l[1], index, p_l_copy[1], 0, sh_length);
418 System.arraycopy(p_r[0], index, p_r_copy[0], 0, sh_length);
419 System.arraycopy(p_r[1], index, p_r_copy[1], 0, sh_length);
420 // 2 - insert value into 'p' (the two control arrays get the same value)
421 p[0][index] = p_l[0][index] = p_r[0][index] = x_p;
422 p[1][index] = p_l[1][index] = p_r[1][index] = y_p;
423 // 3 - copy second half into the array
424 System.arraycopy(p_copy[0], 0, p[0], index+1, sh_length);
425 System.arraycopy(p_copy[1], 0, p[1], index+1, sh_length);
426 System.arraycopy(p_l_copy[0], 0, p_l[0], index+1, sh_length);
427 System.arraycopy(p_l_copy[1], 0, p_l[1], index+1, sh_length);
428 System.arraycopy(p_r_copy[0], 0, p_r[0], index+1, sh_length);
429 System.arraycopy(p_r_copy[1], 0, p_r[1], index+1, sh_length);
431 //add one up
432 this.n_points++;
434 // set the x,y and readjust points
435 calculateBoundingBox();
437 return index;
440 /**Find the closest point to an interpolated point with precision depending upon magnification. The point x_p,y_p is in local coordinates. */
441 protected int findClosestPoint(double x_p, double y_p, double magnification, double bezier_finess) {
442 if (0 == p_i[0].length) return -1; // when none added yet
443 int index = -1;
444 double distance_sq = Double.MAX_VALUE;
445 double distance_sq_i;
446 double max = 12.0D / magnification;
447 max = max * max; //squaring it
448 for (int i=0; i<p_i[0].length; i++) {
449 //see which point is closer (there's no need to calculate the distance by multiplying squares and so on).
450 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);
451 if (distance_sq_i < max && distance_sq_i < distance_sq) {
452 index = i;
453 distance_sq = distance_sq_i;
456 if (-1 != index) {
457 int index_found = (int)((double)index * bezier_finess);
458 if (index < (index_found / bezier_finess)) {
459 index_found--;
461 index = index_found;
463 return index;
466 /**Toggle curve closed/open.*/
467 public void toggleClosed() {
468 if (closed) {
469 closed = false;
470 } else {
471 closed = true;
473 //update database
474 updateInDatabase("closed");
477 protected void generateInterpolatedPoints(double bezier_finess) {
478 if (0 >= n_points) {
479 return;
482 int n = n_points;
483 if (closed && n > 1) {
484 //do the loop for one more
485 n++;
486 //enlarge arrays if needed
487 if (p[0].length == n_points) {
488 enlargeArrays();
490 //add first point to the end (doesn't need to be deleted because n_points hasn't changed)
491 // n_points works as an index here.
492 p[0][n_points] = p[0][0];
493 p[1][n_points] = p[1][0];
494 p_l[0][n_points] = p_l[0][0];
495 p_l[1][n_points] = p_l[1][0];
496 p_r[0][n_points] = p_r[0][0];
497 p_r[1][n_points] = p_r[1][0];
500 // case there's only one point
501 if (1 == n_points) {
502 p_i = new double[2][1];
503 p_i[0][0] = p[0][0];
504 p_i[1][0] = p[1][0];
505 return;
507 // case there's more: interpolate!
508 p_i = new double[2][(int)(n * (1.0D/bezier_finess))];
509 double t, f0, f1, f2, f3;
510 int next = 0;
511 for (int i=0; i<n-1; i++) {
512 for (t=0.0D; t<1.0D; t += bezier_finess) {
513 f0 = (1-t)*(1-t)*(1-t);
514 f1 = 3*t*(1-t)*(1-t);
515 f2 = 3*t*t*(1-t);
516 f3 = t*t*t;
517 p_i[0][next] = f0*p[0][i] + f1*p_r[0][i] + f2*p_l[0][i+1] + f3*p[0][i+1];
518 p_i[1][next] = f0*p[1][i] + f1*p_r[1][i] + f2*p_l[1][i+1] + f3*p[1][i+1];
519 next++;
520 //enlarge if needed (when bezier_finess is not 0.05, it's difficult to predict because of int loss of precision.
521 if (p_i[0].length == next) {
522 double[][] p_i_copy = new double[2][p_i[0].length + 5];
523 System.arraycopy(p_i[0], 0, p_i_copy[0], 0, p_i[0].length);
524 System.arraycopy(p_i[1], 0, p_i_copy[1], 0, p_i[1].length);
525 p_i = p_i_copy;
529 if (p_i[0].length != next) { // 'next' works as a length here
530 //resize back
531 double[][] p_i_copy = new double[2][next];
532 System.arraycopy(p_i[0], 0, p_i_copy[0], 0, next);
533 System.arraycopy(p_i[1], 0, p_i_copy[1], 0, next);
534 p_i = p_i_copy;
539 public void paint(final Graphics2D g, final double magnification, final boolean active, final int channels, final Layer active_layer) {
540 if (0 == n_points) return;
541 if (-1 == n_points) {
542 // load points from the database
543 setupForDisplay();
544 if (-1 == n_points) {
545 Utils.log2("Profile.paint: Some error ocurred, can't load points from database.");
546 return;
549 //arrange transparency
550 Composite original_composite = null;
551 if (alpha != 1.0f) {
552 original_composite = g.getComposite();
553 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
556 // local pointers, since they may be transformed
557 double[][] p = this.p;
558 double[][] p_r = this.p_r;
559 double[][] p_l = this.p_l;
560 double[][] p_i = this.p_i;
561 if (!this.at.isIdentity()) {
562 final Object[] ob = getTransformedData();
563 p = (double[][])ob[0];
564 p_l = (double[][])ob[1];
565 p_r = (double[][])ob[2];
566 p_i = (double[][])ob[3];
568 if (active) {
569 //draw/fill points
570 final int oval_radius = (int)Math.ceil(4 / magnification);
571 final int oval_corr = (int)Math.ceil(3 / magnification);
572 for (int j=0; j<n_points; j++) {
573 DisplayCanvas.drawHandle(g, (int)p[0][j], (int)p[1][j], magnification);
574 g.setColor(this.color);
575 //fill small ovals at control points
576 g.fillOval((int)p_l[0][j] -oval_corr, (int)p_l[1][j] -oval_corr, oval_radius, oval_radius);
577 g.fillOval((int)p_r[0][j] -oval_corr, (int)p_r[1][j] -oval_corr, oval_radius, oval_radius);
578 //draw lines between backbone and control points
579 g.drawLine((int)p[0][j], (int)p[1][j], (int)p_l[0][j], (int)p_l[1][j]);
580 g.drawLine((int)p[0][j], (int)p[1][j], (int)p_r[0][j], (int)p_r[1][j]);
584 //set color
585 g.setColor(this.color);
587 //draw lines between any two consecutive interpolated points
588 for (int i=0; i<p_i[0].length-1; i++) {
589 g.drawLine((int)p_i[0][i], (int)p_i[1][i], (int)p_i[0][i+1], (int)p_i[1][i+1]);
591 //draw last segment between last and first points, only if closed:
592 if (closed) {
593 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]);
596 //Transparency: fix alpha composite back to original.
597 if (null != original_composite) {
598 g.setComposite(original_composite);
602 /**Helper vars for mouse events. It's safe to have them static since only one Profile will be edited at a time.*/
603 static private int index = -1;
604 static private int index_l = -1;
605 static private int index_r = -1;
606 static private boolean is_new_point = false;
608 /**Execute the mousePressed MouseEvent on this Profile.*/
609 public void mousePressed(MouseEvent me, int x_p, int y_p, double mag) {
610 // transform the x_p, y_p to the local coordinates
611 if (!this.at.isIdentity()) {
612 final Point2D.Double po = inverseTransformPoint(x_p, y_p);
613 x_p = (int)po.x;
614 y_p = (int)po.y;
617 final int tool = ProjectToolbar.getToolId();
619 // reset helper vars
620 is_new_point = false;
621 index = index_r = index_l = -1;
623 if (ProjectToolbar.PEN == tool) {
625 //collect vars
626 if (Utils.isControlDown(me) && me.isShiftDown()) {
627 index = findNearestPoint(p, n_points, x_p, y_p);
628 } else {
629 index = findPoint(p, x_p, y_p, mag);
632 if (-1 != index) {
633 if (Utils.isControlDown(me) && me.isShiftDown()) {
634 //delete point
635 removePoint(index);
636 index = index_r = index_l = -1;
637 generateInterpolatedPoints(0.05);
638 repaint(false);
639 return;
640 } else if (me.isAltDown()) {
641 resetControlPoints(index);
642 return;
643 } else if (me.isShiftDown()) {
644 //preconditions: at least 2 points!
645 if (n_points < 2) {
646 return;
648 toggleClosed();
649 generateInterpolatedPoints(0.05);
650 repaint(false);
651 return;
652 } else if (0 == index && n_points > 1 && !closed) {
653 //close curve, reset left control point of the first point and set it up for dragging
654 closed = true;
655 updateInDatabase("closed");
656 p_l[0][0] = p[0][0];
657 p_l[1][0] = p[1][0];
658 index = -1;
659 index_r = -1;
660 index_l = 0; //the first one
661 repaint(false);
662 return;
666 // find if click is on a left control point
667 index_l = findPoint(p_l, x_p, y_p, mag);
668 index_r = -1;
669 // if not, then try on the set of right control points
670 if (-1 == index_l) {
671 index_r = findPoint(p_r, x_p, y_p, mag);
674 //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).
675 if (-1 == index && -1 == index_l && -1 == index_r && !me.isShiftDown() && !me.isAltDown()) {
676 //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.
677 index_l = addPoint(x_p, y_p, mag, 0.05);
678 if (-1 != index_l) is_new_point = true;
679 else if (1 == n_points) {
680 //for the very first point, drag the right control point, not the left.
681 index_r = index_l;
682 index_l = -1;
684 repaint(false);
685 return;
690 /**Execute the mouseDragged MouseEvent on this Profile.*/
691 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) {
692 // transform to the local coordinates
693 if (!this.at.isIdentity()) {
694 final Point2D.Double p = inverseTransformPoint(x_p, y_p);
695 x_p = (int)p.x;
696 y_p = (int)p.y;
697 final Point2D.Double pd = inverseTransformPoint(x_d, y_d);
698 x_d = (int)pd.x;
699 y_d = (int)pd.y;
700 final Point2D.Double pdo = inverseTransformPoint(x_d_old, y_d_old);
701 x_d_old = (int)pdo.x;
702 y_d_old = (int)pdo.y;
705 final int tool = ProjectToolbar.getToolId();
707 if (ProjectToolbar.PEN == tool) {
709 //if a point in the backbone is found, then:
710 if (-1 != index) {
711 if (!me.isAltDown()) {
712 //drag point
713 dragPoint(index, x_d - x_d_old, y_d - y_d_old);
714 } else {
715 //drag both control points symmetrically
716 dragControlPoint(index, x_d, y_d, p_l, p_r, true);
718 generateInterpolatedPoints(0.05);
719 repaint(false);
720 return;
723 //if a control point is found, then drag it, adjusting the other control point non-symmetrically
724 if (-1 != index_r) {
725 dragControlPoint(index_r, x_d, y_d, p_r, p_l, is_new_point);
726 generateInterpolatedPoints(0.05);
727 repaint(false);
728 return;
730 if (-1 != index_l) {
731 dragControlPoint(index_l, x_d, y_d, p_l, p_r, is_new_point);
732 generateInterpolatedPoints(0.05);
733 repaint(false);
734 return;
737 // no points selected. Drag the whole curve on alt down (without affecting linked curves)
738 if (me.isAltDown()) {
739 int dx = x_d - x_d_old;
740 int dy = y_d - y_d_old;
741 this.at.translate(dx, dy);
742 repaint(false);
743 return;
748 /**Execute the mouseReleased MouseEvent on this Profile.*/
749 public void mouseReleased(MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r) {
750 int tool = ProjectToolbar.getToolId();
751 if (ProjectToolbar.PEN == tool) {
752 //generate interpolated points
753 generateInterpolatedPoints(0.05);
754 repaint(); //needed at least for the removePoint, and also for repainting the DisplayablePanel and the DisplayNavigator // TODO this may be redundant with below
757 //update points in database if there was any change
758 if (-1 != index || -1 != index_r || -1 != index_l) {
759 updateInDatabase("points");
760 updateInDatabase("transform+dimensions"); //was: dimensions
761 Display.repaint(layer, this); // the DisplayablePanel
762 } else if (x_r != x_p || y_r != y_p) {
763 updateInDatabase("transform+dimensions");
764 Display.repaint(layer, this); // the DisplayablePanel
766 // reset helper vars
767 is_new_point = false;
768 index = index_r = index_l = -1;
771 protected void calculateBoundingBox() {
772 calculateBoundingBox(true);
775 /**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. */
776 protected void calculateBoundingBox(boolean adjust_position) {
777 if (0 == n_points) {
778 this.width = this.height = 0.0D;
779 layer.updateBucket(this);
780 return;
782 //go over all points and control points and find the max and min
783 // (there's no need to use the interpolated points because the points and control points define the boxes in which the interpolated points are).
784 double min_x = Double.MAX_VALUE;
785 double min_y = Double.MAX_VALUE;
786 double max_x = 0.0D;
787 double max_y = 0.0D;
789 for (int i=0; i<n_points; i++) {
790 if (p[0][i] < min_x) min_x = p[0][i];
791 if (p_l[0][i] < min_x) min_x = p_l[0][i];
792 if (p_r[0][i] < min_x) min_x = p_r[0][i];
793 if (p[1][i] < min_y) min_y = p[1][i];
794 if (p_l[1][i] < min_y) min_y = p_l[1][i];
795 if (p_r[1][i] < min_y) min_y = p_r[1][i];
796 if (p[0][i] > max_x) max_x = p[0][i];
797 if (p_l[0][i] > max_x) max_x = p_l[0][i];
798 if (p_r[0][i] > max_x) max_x = p_r[0][i];
799 if (p[1][i] > max_y) max_y = p[1][i];
800 if (p_l[1][i] > max_y) max_y = p_l[1][i];
801 if (p_r[1][i] > max_y) max_y = p_r[1][i];
804 this.width = max_x - min_x;
805 this.height = max_y - min_y;
807 if (adjust_position) {
808 // now readjust points to make min_x,min_y be the x,y
809 for (int i=0; i<n_points; i++) {
810 p[0][i] -= min_x; p[1][i] -= min_y;
811 p_l[0][i] -= min_x; p_l[1][i] -= min_y;
812 p_r[0][i] -= min_x; p_r[1][i] -= min_y;
814 for (int i=0; i<p_i[0].length; i++) {
815 p_i[0][i] -= min_x; p_i[1][i] -= min_y;
817 this.at.translate(min_x, min_y); // not using super.translate(...) because a preConcatenation is not needed; here we deal with the data.
818 updateInDatabase("transform");
820 layer.updateBucket(this);
821 updateInDatabase("dimensions");
825 public void repaint() {
826 repaint(true);
829 /**Repaints in the given ImageCanvas only the area corresponding to the bounding box of this Profile. */
830 public void repaint(boolean repaint_navigator) {
831 //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.
832 Rectangle box = getBoundingBox(null);
833 calculateBoundingBox();
834 box.add(getBoundingBox(null));
835 Display.repaint(layer, this, box, 5, repaint_navigator);
838 /**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. */
839 public boolean containsPoint(final int x_p, final int y_p) {
840 // as in getPerimeter():
841 int n_i = p_i[0].length;
842 int[] intx = new int[n_i];
843 int[] inty = new int[n_i];
844 for (int i=0; i<n_i; i++) {
845 intx[i] = (int)p_i[0][i];
846 inty[i] = (int)p_i[1][i];
848 Polygon polygon = new Polygon(intx, inty, n_i);
849 return polygon.contains(x_p, y_p);
852 /**Release all memory resources taken by this object.*/
853 public void destroy() {
854 super.destroy();
855 p = null;
856 p_l = null;
857 p_r = null;
858 p_i = null;
861 /**Make this object ready to be painted.*/
862 private void setupForDisplay() {
863 // load points
864 if (null == p || null == p_l || null == p_r) {
865 //load points from database
866 double[][][] bezarr = project.getLoader().fetchBezierArrays(this.id);
867 if (null == bezarr) {
868 Utils.log("Profile.setupForDisplay: could not load the bezier points from the database for id=" + this.id);
869 this.p_i = new double[0][0];
870 return;
872 p_l = bezarr[0];
873 p = bezarr[1];
874 p_r = bezarr[2];
875 n_points = p[0].length;
876 // recreate interpolated points
877 generateInterpolatedPoints(0.05); //TODO the 0.05 bezier finess, read the value from the Project perhaps.
881 /**Cache this Profile if needed.*/ //Is there much sense in caching Profile objects? Unless you have thousands ... and that CAN be the case!
882 public void cache() {
883 //TODO
886 /**Release memory resources used by this object: namely the arrays of points, which can be reloaded with a call to setupForDisplay()*/
887 public void flush() {
888 p = null;
889 p_l = null;
890 p_r = null;
891 p_i = null;
892 n_points = -1; // flag that points exist (and need to be reloaded)
895 /** The perimeter of this profile, in integer precision. */
896 public Polygon getPerimeter() {
897 if (-1 == n_points) setupForDisplay();
898 if (null == p_i) return null; // has been flushed, incorrect access! This is a patch.
900 // transform
901 double[][] p_i = this.p_i;
902 if (!this.at.isIdentity()) p_i = transformPoints(this.p_i);
904 int n_i = p_i[0].length;
905 int[] intx = new int[n_i];
906 int[] inty = new int[n_i];
907 for (int i=0; i<n_i; i++) {
908 intx[i] = (int)p_i[0][i];
909 inty[i] = (int)p_i[1][i];
911 return new Polygon(intx, inty, n_i);
914 /** 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. */
915 public void toShapesFile(StringBuffer data, String group, String color, double z_scale) {
916 if (-1 == n_points) setupForDisplay(); // reload
917 // local pointers, since they may be transformed
918 double[][] p = this.p;
919 double[][] p_r = this.p_r;
920 double[][] p_l = this.p_l;
921 if (!this.at.isIdentity()) {
922 final Object[] ob = getTransformedData();
923 p = (double[][])ob[0];
924 p_l = (double[][])ob[1];
925 p_r = (double[][])ob[2];
927 final double z = layer.getZ();
928 final char l = '\n';
929 data.append("type=bezier").append(l)
930 .append("name=").append(project.getMeaningfulTitle(this)).append(l)
931 .append("group=").append(group).append(l)
932 .append("color=").append(color).append(l)
933 .append("supergroup=").append("null").append(l)
934 .append("supercolor=").append("null").append(l)
935 .append("in slice=").append(z * z_scale).append(l) // fake, this is now the absolute z coordinate
936 .append("curve_closed=").append(true).append(l) // must!
937 .append("density field=").append(false).append(l) // must!
939 for (int i=0; i<n_points; i++) {
940 data.append("p x=").append(p[0][i]).append(l)
941 .append("p y=").append(p[1][i]).append(l)
942 .append("p_r x=").append(p_r[0][i]).append(l)
943 .append("p_r y=").append(p_r[1][i]).append(l)
944 .append("p_l x=").append(p_l[0][i]).append(l)
945 .append("p_l y=").append(p_l[1][i]).append(l)
950 public void exportSVG(StringBuffer data, double z_scale, String indent) {
951 String in = indent + "\t";
952 if (-1 == n_points) setupForDisplay(); // reload
953 if (0 == n_points) return;
954 String[] RGB = Utils.getHexRGBColor(color);
955 final double[] a = new double[6];
956 at.getMatrix(a);
957 data.append(indent).append("<path\n")
958 .append(in).append("type=\"profile\"\n")
959 .append(in).append("id=\"").append(id).append("\"\n")
960 .append(in).append("transform=\"matrix(").append(a[0]).append(',')
961 .append(a[1]).append(',')
962 .append(a[2]).append(',')
963 .append(a[3]).append(',')
964 .append(a[4]).append(',')
965 .append(a[5]).append(")\"\n")
966 .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")
967 .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]);
968 for (int i=0; i<n_points-1; i++) {
969 data.append(' ').append(p[0][i]).append(',').append(p[1][i])
970 .append(" C ").append(p_r[0][i]).append(',').append(p_r[1][i])
971 .append(' ').append(p_l[0][i+1]).append(',').append(p_l[1][i+1])
974 data.append(' ').append(p[0][n_points-1]).append(',').append(p[1][n_points-1]);
975 if (closed) {
976 data.append(" C ").append(p_r[0][n_points-1]).append(',').append(p_r[1][n_points-1])
977 .append(' ').append(p_l[0][0]).append(',').append(p_l[1][0])
978 .append(' ').append(p[0][0]).append(',').append(p[1][0])
979 .append(" z")
982 data.append("\"\n")
983 .append(in).append("z=\"").append(layer.getZ() * z_scale).append("\"\n")
984 .append(in).append("links=\"")
986 if (null != hs_linked && 0 != hs_linked.size()) {
987 int ii = 0;
988 int len = hs_linked.size();
989 for (Iterator it = hs_linked.iterator(); it.hasNext(); ) {
990 Object ob = it.next();
991 data.append(((DBObject)ob).getId());
992 if (ii != len-1) data.append(',');
993 ii++;
996 data.append("\"\n")
997 .append(indent).append("/>\n")
1001 /** Returns a triple array, each containing a [2][n_points] array specifiying the x,y of each left control point, backbone point and right control point respectively.*/
1002 public double[][][] getBezierArrays() {
1003 //assumes the profile is a Bezier curve.
1004 //put points and control points into PGpoint objects, as: LPRLPRLPR... (L = left control point, P = backbone point, R = right control point)
1005 if (-1 == n_points) setupForDisplay(); // reload
1006 final double[][][] b = new double[3][][];
1007 b[0] = p_l;
1008 b[1] = p;
1009 b[2] = p_r;
1010 return b;
1013 public boolean isDeletable() {
1014 return 0 == n_points;
1017 /** Returns true if it's linked to at least one patch in the same Layer. Otherwise returns false. */
1018 public boolean isLinked() {
1019 if (null == hs_linked || hs_linked.isEmpty()) return false;
1020 for (Iterator it = hs_linked.iterator(); it.hasNext(); ) {
1021 Displayable d = (Displayable)it.next();
1022 if (d instanceof Patch && d.layer.equals(this.layer)) return true;
1024 return false;
1027 /** Returns false if the target_layer contains a profile that is directly linked to this profile. */
1028 public boolean canSendTo(Layer target_layer) {
1029 if (null == hs_linked || hs_linked.isEmpty()) return false;
1030 for (Iterator it = hs_linked.iterator(); it.hasNext(); ) {
1031 Displayable d = (Displayable)it.next();
1032 if (d instanceof Profile && d.layer.equals(target_layer)) return false;
1034 return true;
1037 protected double[][] getFirstPoint()
1039 if (0 == n_points) return null;
1040 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]}};
1043 protected double[][] getLastPoint()
1045 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]}};
1048 public boolean hasPoints() {
1049 return 0 != n_points;
1052 protected void setPoints(double[][] p_l, double[][] p, double[][] p_r) {
1053 this.p_l = p_l;
1054 this.p = p;
1055 this.p_r = p_r;
1056 this.n_points = p_l[0].length;
1057 this.generateInterpolatedPoints(0.05);
1060 public void setPoints(double[][] p_l, double[][] p, double[][] p_r, boolean update) {
1061 setPoints(p_l, p, p_r);
1062 calculateBoundingBox();
1063 if (update) {
1064 updateInDatabase("points");
1065 repaint(true);
1069 protected void addPointsAtBegin(double[][] new_p_l, double[][] new_p, double[][] new_p_r) {
1070 double[][] tmp_p_l = new double[2][p_l[0].length + new_p_l[0].length];
1071 double[][] tmp_p = new double[2][p[0].length + new_p[0].length];
1072 double[][] tmp_p_r = new double[2][p_r[0].length + new_p_r[0].length];
1073 int i = 0;
1074 for (; i < new_p_l[0].length; i++) {
1075 tmp_p_l[0][i] = new_p_l[0][i];
1076 tmp_p_l[1][i] = new_p_l[1][i];
1077 tmp_p[0][i] = new_p[0][i];
1078 tmp_p[1][i] = new_p[1][i];
1079 tmp_p_r[0][i] = new_p_r[0][i];
1080 tmp_p_r[1][i] = new_p_r[1][i];
1082 for (int j = 0; j < n_points; j++, i++) {
1083 tmp_p_l[0][i] = p_l[0][j];
1084 tmp_p_l[1][i] = p_l[1][j];
1085 tmp_p[0][i] = p[0][j];
1086 tmp_p[1][i] = p[1][j];
1087 tmp_p_r[0][i] = p_r[0][j];
1088 tmp_p_r[1][i] = p_r[1][j];
1090 this.n_points += new_p_l[0].length;
1092 p_l = tmp_p_l;
1093 p = tmp_p;
1094 p_r = tmp_p_r;
1095 this.generateInterpolatedPoints(0.05);
1098 protected void addPointsAtEnd(double[][] new_p_l, double[][] new_p, double[][] new_p_r) {
1099 double[][] tmp_p_l = new double[2][p_l[0].length + new_p_l[0].length];
1100 double[][] tmp_p = new double[2][p[0].length + new_p[0].length];
1101 double[][] tmp_p_r = new double[2][p_r[0].length + new_p_r[0].length];
1102 int i = 0;
1103 for (; i < n_points; i++) {
1104 tmp_p_l[0][i] = p_l[0][i];
1105 tmp_p_l[1][i] = p_l[1][i];
1106 tmp_p[0][i] = p[0][i];
1107 tmp_p[1][i] = p[1][i];
1108 tmp_p_r[0][i] = p_r[0][i];
1109 tmp_p_r[1][i] = p_r[1][i];
1112 for (int j = 0; j < new_p_l[0].length; i++,j++) {
1113 tmp_p_l[0][i] = new_p_l[0][j];
1114 tmp_p_l[1][i] = new_p_l[1][j];
1115 tmp_p[0][i] = new_p[0][j];
1116 tmp_p[1][i] = new_p[1][j];
1117 tmp_p_r[0][i] = new_p_r[0][j];
1118 tmp_p_r[1][i] = new_p_r[1][j];
1120 this.n_points += new_p_l[0].length;
1122 p_l = tmp_p_l;
1123 p = tmp_p;
1124 p_r = tmp_p_r;
1125 this.generateInterpolatedPoints(0.05);
1128 public int getNearestPointIndex(double x_p, double y_p) {
1129 int ret = -1;
1130 double minDist = Double.POSITIVE_INFINITY;
1131 for (int i = 0; i < this.n_points; i++) {
1132 double dx = this.p[0][i]-x_p;
1133 double dy = this.p[1][i]-y_p;
1134 double dist = dx*dx+dy*dy;
1135 if(dist < minDist)
1137 minDist = dist;
1138 ret = i;
1141 return ret;
1144 public void insertBetween(int startIndex, int endIndex, double[][] tmp_p_l, double[][] tmp_p, double[][] tmp_p_r){
1145 if(endIndex < startIndex)
1147 for (int i = 0; i < 2; i++) {
1148 for (int j = 0; j < tmp_p[0].length/2; j++) {
1149 double tmppl = tmp_p_l[i][j];
1150 double tmpp = tmp_p[i][j];
1151 double tmppr = tmp_p_r[i][j];
1153 tmp_p_r[i][j] = tmp_p_l[i][tmp_p_l[0].length -1- j];
1154 tmp_p[i][j] = tmp_p[i][tmp_p[0].length -1- j];
1155 tmp_p_l[i][j] = tmp_p_r[i][tmp_p_r[0].length -1- j];
1157 tmp_p_r[i][tmp_p_l[0].length -1- j]=tmppl;
1158 tmp_p[i][tmp_p[0].length -1- j]=tmpp;
1159 tmp_p_l[i][tmp_p_r[0].length -1- j]=tmppr;
1163 int tmp = startIndex;
1164 startIndex = endIndex;
1165 endIndex = tmp;
1169 double[][] beginning_p_l;
1170 double[][] beginning_p;
1171 double[][] beginning_p_r;
1173 double[][] ending_p_l;
1174 double[][] ending_p;
1175 double[][] ending_p_r;
1177 if(endIndex - startIndex < n_points + startIndex - endIndex || closed == false)
1179 beginning_p_l = new double [2][startIndex+1];
1180 beginning_p = new double [2][startIndex+1];
1181 beginning_p_r = new double [2][startIndex+1];
1183 ending_p_l = new double [2][n_points- endIndex];
1184 ending_p = new double [2][n_points- endIndex];
1185 ending_p_r = new double [2][n_points- endIndex];
1187 for(int i = 0 ; i <= startIndex ; i++)
1189 for (int j = 0; j < 2; j++) {
1190 beginning_p_l [j][i] = this.p_l[j][i];
1191 beginning_p [j][i] = this.p[j][i];
1192 beginning_p_r [j][i] = this.p_r[j][i];
1195 for(int i = endIndex ; i < this.n_points ; i++)
1197 for (int j = 0; j < 2; j++) {
1198 ending_p_l [j][i-endIndex] = this.p_l[j][i];
1199 ending_p [j][i-endIndex] = this.p[j][i];
1200 ending_p_r [j][i-endIndex] = this.p_r[j][i];
1203 System.out.println("1");
1205 else
1207 beginning_p_l = new double [2][endIndex-startIndex + 1];
1208 beginning_p = new double [2][ endIndex-startIndex + 1 ];
1209 beginning_p_r = new double [2][endIndex-startIndex + 1];
1211 ending_p_l = new double [2][0];
1212 ending_p = new double [2][0];
1213 ending_p_r = new double [2][0];
1215 for(int i = startIndex ; i <= endIndex ; i++)
1217 for (int j = 0; j < 2; j++) {
1218 beginning_p_r [j][endIndex - i] = this.p_l[j][i];
1219 beginning_p [j][endIndex - i] = this.p[j][i];
1220 beginning_p_l [j][endIndex - i] = this.p_r[j][i];
1224 System.out.println("2");
1230 double[][] new_p_l = new double[2][beginning_p_l[0].length + ending_p_l[0].length + tmp_p_l[0].length];
1231 double[][] new_p = new double[2][beginning_p[0].length + ending_p[0].length + tmp_p[0].length];
1232 double[][] new_p_r = new double[2][beginning_p_r[0].length + ending_p_r[0].length + tmp_p_r[0].length];
1234 for (int i = 0; i < beginning_p[0].length; i++) {
1235 for (int j = 0; j < 2; j++) {
1236 new_p_l[j][i] = beginning_p_l[j][i];
1237 new_p[j][i] = beginning_p[j][i];
1238 new_p_r[j][i] = beginning_p_r[j][i];
1241 for (int i = 0; i < tmp_p[0].length; i++) {
1242 for (int j = 0; j < 2; j++) {
1243 new_p_l[j][i+beginning_p[0].length] = tmp_p_l[j][i];
1244 new_p[j][i+beginning_p[0].length] = tmp_p[j][i];
1245 new_p_r[j][i+beginning_p[0].length] = tmp_p_r[j][i];
1248 for (int i = 0; i < ending_p[0].length; i++) {
1249 for (int j = 0; j < 2; j++) {
1250 new_p_l[j][i+beginning_p[0].length+tmp_p[0].length] = ending_p_l[j][i];
1251 new_p[j][i+beginning_p[0].length+tmp_p[0].length] = ending_p[j][i];
1252 new_p_r[j][i+beginning_p[0].length+tmp_p[0].length] = ending_p_r[j][i];
1255 this.n_points = new_p[0].length;
1256 this.p_l = new_p_l;
1257 this.p = new_p;
1258 this.p_r = new_p_r;
1259 this.calculateBoundingBox();
1260 this.generateInterpolatedPoints(0.05);
1263 public void printPoints() {
1264 System.out.println("#####\nw,h: " + width + "," + height);
1265 for (int i=0; i<n_points; i++) {
1266 System.out.println("x,y: " + p[0][i] + " , " + p[1][i]);
1268 System.out.println("\n");
1272 /** x,y is the cursor position in offscreen coordinates. */ // COPIED from the Pipe
1273 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.
1274 if (-1 != index) {
1275 // #$#@$%#$%!!! 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).
1277 double dx = p_l[0][index] - p[0][index];
1278 double dy = p_l[1][index] - p[1][index];
1279 p_l[0][index] = cx + dx;
1280 p_l[1][index] = cy + dy;
1281 dx = p_r[0][index] - p[0][index];
1282 dy = p_r[1][index] - p[1][index];
1283 p_r[0][index] = cx + dx;
1284 p_r[1][index] = cy + dy;
1285 p[0][index] = cx;
1286 p[1][index] = cy;
1288 } else if (-1 != index_l) {
1289 p_l[0][index_l] = cx;
1290 p_l[1][index_l] = cy;
1291 } else if (-1 != index_r) {
1292 p_r[0][index_r] = cx;
1293 p_r[1][index_r] = cy;
1294 } else {
1295 // drag the whole pipe
1296 // CONCEPTUALLY WRONG, what happens when not dragging the pipe, on mouseEntered? Disaster!
1297 //drag(cx - x_p, cy - y_p);
1301 public void exportXML(StringBuffer sb_body, String indent, Object any) {
1302 sb_body.append(indent).append("<t2_profile\n");
1303 String in = indent + "\t";
1304 super.exportXML(sb_body, in, any);
1305 if (-1 == n_points) setupForDisplay(); // reload
1306 String[] RGB = Utils.getHexRGBColor(color);
1307 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");
1308 if (n_points > 0) {
1309 sb_body.append(in).append("d=\"M");
1310 for (int i=0; i<n_points-1; i++) {
1311 sb_body.append(' ').append(p[0][i]).append(',').append(p[1][i])
1312 .append(" C ").append(p_r[0][i]).append(',').append(p_r[1][i])
1313 .append(' ').append(p_l[0][i+1]).append(',').append(p_l[1][i+1])
1316 sb_body.append(' ').append(p[0][n_points-1]).append(',').append(p[1][n_points-1]);
1317 if (closed) {
1318 sb_body.append(" C ").append(p_r[0][n_points-1]).append(',').append(p_r[1][n_points-1])
1319 .append(' ').append(p_l[0][0]).append(',').append(p_l[1][0])
1320 .append(' ').append(p[0][0]).append(',').append(p[1][0])
1321 .append(" z")
1324 sb_body.append("\"\n");
1326 sb_body.append(indent).append(">\n");
1327 super.restXML(sb_body, in, any);
1328 sb_body.append(indent).append("</t2_profile>\n");
1331 static public void exportDTD(StringBuffer sb_header, HashSet hs, String indent) {
1332 String type = "t2_profile";
1333 if (hs.contains(type)) return;
1334 hs.add(type);
1335 sb_header.append(indent).append("<!ELEMENT t2_profile (").append(Displayable.commonDTDChildren()).append(")>\n");
1336 Displayable.exportDTD(type, sb_header, hs, indent);
1337 sb_header.append(indent).append(TAG_ATTR1).append(type).append(" d").append(TAG_ATTR2)
1341 /** Returns the interpolated points as a Perimeter2D. */
1342 public Perimeter2D getPerimeter2D() {
1343 if (-1 == n_points) setupForDisplay(); // reload
1344 if (0 == n_points) return null;
1345 if (0 == p_i[0].length) generateInterpolatedPoints(0.05);
1346 double[] x = new double[p_i[0].length];
1347 double[] y = new double[x.length];
1348 System.arraycopy(p_i[0], 0, x, 0, x.length);
1349 System.arraycopy(p_i[1], 0, y, 0, x.length); // breaking symmetry in purpose, same result
1350 return new Perimeter2D(x, y, layer.getZ(), this.closed);
1353 public void keyPressed(KeyEvent ke) {
1354 super.keyPressed(ke);
1355 if (ke.isConsumed()) return;
1356 int key_code = ke.getKeyCode();
1357 Rectangle box = null;
1358 switch(key_code) {
1359 case KeyEvent.VK_X: // remove all points
1360 if (0 == ke.getModifiers() && (ProjectToolbar.getToolId() == ProjectToolbar.PEN || ProjectToolbar.getToolId() == ProjectToolbar.PENCIL)) {
1361 box = getBoundingBox(box);
1362 n_points = 0;
1363 this.p_i = new double[2][0];
1364 calculateBoundingBox(true);
1365 ke.consume();
1366 if (closed) toggleClosed();
1367 updateInDatabase("points");
1369 break;
1371 if (ke.isConsumed()) {
1372 Display.repaint(this.layer, box, 5);
1376 public void setColor(Color c) {
1377 // propagate to al linked profiles within the same profile_list
1378 setColor(c, new HashSet());
1381 /** Exploits the fact that Profile instances among the directly linked as returned by getLinked(Profile.class) will be members of the same profile_list. */
1382 private void setColor(Color c, HashSet hs_done) {
1383 if (hs_done.contains(this)) return;
1384 hs_done.add(this);
1385 super.setColor(c);
1386 HashSet hs = getLinked(Profile.class);
1387 for (Iterator it = hs.iterator(); it.hasNext(); ) {
1388 Profile p = (Profile)it.next();
1389 p.setColor(c, hs_done);
1393 /** Performs a deep copy of this object, unlocked and visible. */
1394 public Displayable clone(final Project pr, final boolean copy_id) {
1395 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
1396 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());
1397 // The data:
1398 if (-1 == n_points) setupForDisplay(); // load data
1399 copy.n_points = n_points;
1400 copy.p = new double[][]{(double[])this.p[0].clone(), (double[])this.p[1].clone()};
1401 copy.p_l = new double[][]{(double[])this.p_l[0].clone(), (double[])this.p_l[1].clone()};
1402 copy.p_r = new double[][]{(double[])this.p_r[0].clone(), (double[])this.p_r[1].clone()};
1403 copy.p_i = new double[][]{(double[])this.p_i[0].clone(), (double[])this.p_i[1].clone()};
1404 // add
1405 copy.addToDatabase();
1407 return copy;
1410 private Object[] getTransformedData() {
1411 final double[][] p = transformPoints(this.p);
1412 final double[][] p_l = transformPoints(this.p_l);
1413 final double[][] p_r = transformPoints(this.p_r);
1414 final double[][] p_i = transformPoints(this.p_i);
1415 return new Object[]{p, p_l, p_r, p_i};
1418 /** 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. */
1419 static public List generateTriangles(final ProjectThing pt, final double scale) {
1420 if (!pt.getType().equals("profile_list")) {
1421 Utils.log2("Profile: ignoring unhandable ProjectThing type.");
1422 return null;
1424 ArrayList al = pt.getChildren(); // should be sorted by Z already
1425 if (al.size() < 2) {
1426 Utils.log("profile_list " + pt + " has less than two profiles: can't render in 3D.");
1427 return null;
1429 // collect all Profile
1430 final HashSet hs = new HashSet();
1431 for (Iterator it = al.iterator(); it.hasNext(); ) {
1432 Thing child = (Thing)it.next();
1433 Object ob = child.getObject();
1434 if (ob instanceof Profile) {
1435 hs.add(ob);
1436 } else {
1437 Utils.log2("Render: skipping non Profile class child");
1440 // Create sublists of profiles, following the chain of links.
1441 final Profile[] p = new Profile[hs.size()];
1442 hs.toArray(p);
1443 // find if at least one is visible
1444 boolean hidden = true;
1445 for (int i=0; i<p.length; i++) {
1446 if (p[i].visible) {
1447 hidden = false;
1448 break;
1451 if (hidden) return null;
1452 // collect starts and ends
1453 final HashSet hs_bases = new HashSet();
1454 final HashSet hs_done = new HashSet();
1455 final List triangles = new ArrayList();
1456 do {
1457 Profile base = null;
1458 // choose among existing bases
1459 if (hs_bases.size() > 0) {
1460 base = (Profile)hs_bases.iterator().next();
1461 } else {
1462 // find a new base, simply by taking the lowest Z or remaining profiles
1463 double min_z = Double.MAX_VALUE;
1464 for (int i=0; i<p.length; i++) {
1465 if (hs_done.contains(p[i])) continue;
1466 double z = p[i].getLayer().getZ();
1467 if (z < min_z) {
1468 min_z = z;
1469 base = p[i];
1472 // add base
1473 if (null != base) hs_bases.add(base);
1475 if (null == base) {
1476 Utils.log2("No more bases.");
1477 break;
1479 // crawl list to get a sequence of profiles in increasing or decreasing Z order, but not mixed z trends
1480 final ArrayList al_profiles = new ArrayList();
1481 //Utils.log2("Calling accumulate for base " + base);
1482 al_profiles.add(base);
1483 final Profile last = accumulate(hs_done, al_profiles, base, 0);
1484 // if the trend was not empty, add it
1485 if (last != base) {
1486 // count as done
1487 hs_done.addAll(al_profiles);
1488 // add new possible base (which may have only 2 links if it was from a broken Z trend)
1489 hs_bases.add(last);
1490 // create 3D object from base to base
1491 final Profile[] profiles = new Profile[al_profiles.size()];
1492 al_profiles.toArray(profiles);
1493 List tri = makeTriangles(profiles, scale);
1494 if (null != tri) triangles.addAll(tri);
1495 } else {
1496 // remove base
1497 hs_bases.remove(base);
1499 } while (0 != hs_bases.size());
1501 return triangles;
1504 /** Recursive; returns the last added profile. */
1505 static private Profile accumulate(final HashSet hs_done, final ArrayList al, final Profile step, int z_trend) {
1506 final HashSet hs_linked = step.getLinked(Profile.class);
1507 if (al.size() > 1 && hs_linked.size() > 2) {
1508 // base found
1509 return step;
1511 double step_z = step.getLayer().getZ();
1512 Profile next_step = null;
1513 boolean started = false;
1514 for (Iterator it = hs_linked.iterator(); it.hasNext(); ) {
1515 Object ob = it.next();
1516 // loop only one cycle, to move only in one direction
1517 if (al.contains(ob) || started || hs_done.contains(ob)) continue;
1518 started = true;
1519 next_step = (Profile)ob;
1520 double next_z = next_step.getLayer().getZ();
1521 if (0 == z_trend) {
1522 // define trend
1523 if (next_z > step_z) {
1524 z_trend = 1;
1525 } else {
1526 z_trend = -1;
1528 // add!
1529 al.add(next_step);
1530 } else {
1531 // if the z trend is broken, finish
1532 if ( (next_z > step_z && 1 == z_trend)
1533 || (next_z < step_z && -1 == z_trend) ) {
1534 // z trend continues
1535 al.add(next_step);
1536 } else {
1537 // z trend broken
1538 next_step = null;
1542 Profile last = step;
1543 if (null != next_step) {
1544 hs_done.add(next_step);
1545 last = accumulate(hs_done, al, next_step, z_trend);
1547 return last;
1550 /** Make a mesh as a calibrated list of 3D triangles.*/
1551 static private List<Point3f> makeTriangles(final Profile[] p, final double scale) {
1552 try {
1553 final VectorString2D[] sv = new VectorString2D[p.length];
1554 boolean closed = true; // dummy initialization
1555 final Calibration cal = p[0].getLayerSet().getCalibration();
1556 for (int i=0; i<p.length; i++) {
1557 if (-1 == p[i].n_points) p[i].setupForDisplay();
1558 if (0 == p[i].n_points) continue;
1559 if (0 == i) closed = p[i].closed;
1560 else if (p[i].closed != closed) {
1561 Utils.log2("All profiles should be either open or closed, not mixed.");
1562 return null;
1564 double[][] pi = p[i].transformPoints(p[i].p_i);
1565 final double[] x = (double[])pi[0].clone();
1566 final double[] y = (double[])pi[1].clone();
1567 pi = null;
1568 if (1 != scale) {
1569 for (int k=0; k<x.length; k++) {
1570 x[k] *= scale;
1571 y[k] *= scale;
1574 sv[i] = new VectorString2D(x, y, p[i].layer.getZ(), p[i].closed);
1575 sv[i].calibrate(cal);
1577 return SkinMaker.generateTriangles(sv, -1, -1, closed);
1578 } catch (Exception e) {
1579 IJError.print(e);
1581 return null;
1584 protected boolean remove2(boolean check) {
1585 return project.getProjectTree().remove(check, project.findProjectThing(this), null); // will call remove(check) here
1588 /** Calibrated for pixel width only (that is, it assumes pixel aspect ratio 1:1), in units as specified at getLayerSet().getCalibration().getUnit() */
1589 public double computeLength() {
1590 if (-1 == n_points || 0 == this.p_i[0].length) setupForDisplay();
1591 if (this.p_i[0].length < 2) return 0;
1592 final double[][] p_i = transformPoints(this.p_i);
1593 double len = 0;
1594 for (int i=1; i<p_i[0].length; i++) {
1595 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));
1597 if (closed) {
1598 int last = p[0].length -1;
1599 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));
1601 // to calibrate for pixelWidth and pixelHeight, I'd have to multiply each x,y values above separately
1602 return len * getLayerSet().getCalibration().pixelWidth;
1605 /** Calibrated, in units as specified at getLayerSet().getCalibration().getUnit() -- returns zero if this profile is not closed. */
1606 public double computeArea() {
1607 if (-1 == n_points) setupForDisplay();
1608 if (n_points < 2) return 0;
1609 if (!closed) return 0;
1610 if (0 == p_i[0].length) generateInterpolatedPoints(0.05);
1611 Calibration cal = getLayerSet().getCalibration();
1612 return M.measureArea(new Area(getPerimeter()), getProject().getLoader()) * cal.pixelWidth * cal.pixelHeight;
1615 /** 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. */
1616 public ResultsTable measure(ResultsTable rt) {
1617 if (null == rt) rt = Utils.createResultsTable("Profile results", new String[]{"id", "length", "side surface: length x thickness", "volume: area x thickness", "name-id"});
1618 if (-1 == n_points) setupForDisplay();
1619 if (n_points < 2) return null;
1620 if (0 == p_i[0].length) generateInterpolatedPoints(0.05);
1621 final Calibration cal = getLayerSet().getCalibration();
1622 // computeLength returns a calibrated length, so only calibrate the layer thickness:
1623 final double len = computeLength();
1624 final double surface_flat = len * layer.getThickness() * cal.pixelWidth;
1625 rt.incrementCounter();
1626 rt.addLabel("units", cal.getUnit());
1627 rt.addValue(0, id);
1628 rt.addValue(1, len);
1629 rt.addValue(2, surface_flat);
1630 final double volume = closed ? computeArea() * layer.getThickness() * cal.pixelWidth : 0;
1631 rt.addValue(3, volume);
1632 rt.addValue(4, getNameId());
1633 return rt;
1636 /** Assumes Z-coord sorted list of profiles, as stored in a "profile_list" ProjectThing type. . */
1637 static public ResultsTable measure(final Profile[] profiles, ResultsTable rt, final long profile_list_id) {
1638 Utils.log2("profiles.length" + profiles.length);
1639 if (null == profiles || 0 == profiles.length) return null;
1640 if (1 == profiles.length) {
1641 // don't measure if there is only one
1642 return rt;
1644 if (null == rt) rt = Utils.createResultsTable("Profile list results", new String[]{"id", "interpolated surface", "surface: sum of length x thickness", "volume", "name-id"});
1645 Calibration cal = profiles[0].getLayerSet().getCalibration();
1646 // else, interpolate skin and measure each triangle
1647 List<Point3f> tri = makeTriangles(profiles, 1.0); // already calibrated
1648 final int n_tri = tri.size();
1649 if (0 != n_tri % 3) {
1650 Utils.log("Profile.measure error: triangle verts list not a multiple of 3 for profile list id " + profile_list_id);
1651 return rt;
1653 // Surface: calibrated sum of the area of all triangles in the mesh.
1654 double surface = 0;
1655 for (int i=2; i<n_tri; i+=3) {
1656 surface += M.measureArea(tri.get(i-2), tri.get(i-1), tri.get(i));
1658 // add capping ends
1659 double area_first = profiles[0].computeArea();
1660 double area_last = profiles[profiles.length-1].computeArea();
1661 if (profiles[0].closed) surface += area_first;
1662 if (profiles[profiles.length-1].closed) surface += area_last;
1664 // calibrate surface measurement
1665 surface *= Math.pow(cal.pixelWidth, 3);
1667 // Surface flat: sum of the perimeter lengths times the layer thickness
1668 double surface_flat = 0;
1669 for (int i=0; i<profiles.length; i++) {
1670 if (0 == profiles[i].p_i[0].length) profiles[i].generateInterpolatedPoints(0.05);
1671 surface_flat += profiles[i].computeLength() * profiles[i].layer.getThickness() * cal.pixelWidth;
1674 // Volume: area times layer thickness
1675 double volume = area_first * profiles[0].layer.getThickness();
1676 for (int i=1; i<profiles.length-1; i++) {
1677 volume += profiles[i].computeArea() * profiles[i].layer.getThickness();
1679 volume += area_last * profiles[profiles.length-1].layer.getThickness();
1681 // calibrate volume: the Z is still in pixels
1682 volume *= cal.pixelWidth;
1684 rt.incrementCounter();
1685 rt.addLabel("units", cal.getUnit());
1686 rt.addValue(0, profile_list_id);
1687 rt.addValue(1, surface);
1688 rt.addValue(2, surface_flat);
1689 rt.addValue(3, volume);
1690 double nameid = 0;
1691 try {
1692 nameid = Double.parseDouble(profiles[0].project.findProjectThing(profiles[0]).getParent().getTitle());
1693 } catch (NumberFormatException nfe) {}
1694 rt.addValue(4, nameid);
1695 return rt;
1698 @Override
1699 final Class getInternalDataPackageClass() {
1700 return DPProfile.class;
1703 @Override
1704 synchronized Object getDataPackage() {
1705 return new DPProfile(this);
1708 static private final class DPProfile extends Displayable.DataPackage {
1709 final double[][] p, p_l, p_r, p_i;
1710 final boolean closed;
1712 DPProfile(final Profile profile) {
1713 super(profile);
1714 // store copies of all arrays
1715 this.p = new double[][]{Utils.copy(profile.p[0], profile.n_points), Utils.copy(profile.p[1], profile.n_points)};
1716 this.p_r = new double[][]{Utils.copy(profile.p_r[0], profile.n_points), Utils.copy(profile.p_r[1], profile.n_points)};
1717 this.p_l = new double[][]{Utils.copy(profile.p_l[0], profile.n_points), Utils.copy(profile.p_l[1], profile.n_points)};
1718 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)};
1719 this.closed = profile.closed;
1721 @Override
1722 final boolean to2(final Displayable d) {
1723 super.to1(d);
1724 final Profile profile = (Profile)d;
1725 final int len = p[0].length; // == n_points, since it was cropped on copy
1726 profile.p = new double[][]{Utils.copy(p[0], len), Utils.copy(p[1], len)};
1727 profile.n_points = p[0].length;
1728 profile.p_r = new double[][]{Utils.copy(p_r[0], len), Utils.copy(p_r[1], len)};
1729 profile.p_l = new double[][]{Utils.copy(p_l[0], len), Utils.copy(p_l[1], len)};
1730 profile.p_i = new double[][]{Utils.copy(p_i[0], p_i[0].length), Utils.copy(p_i[1], p_i[1].length)};
1731 profile.closed = closed;
1732 return true;