Removed numerous debug msg from AreaList and added comment.
[trakem2.git] / ini / trakem2 / display / AreaList.java
blob8f8c195adeffd16f95737b9c11ad0e2e18080488
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;
26 import ij.IJ;
27 import ij.gui.OvalRoi;
28 import ij.gui.Roi;
29 import ij.gui.ShapeRoi;
30 import ij.gui.PolygonRoi;
31 import ij.gui.Toolbar;
32 import ij.gui.GenericDialog;
33 import ij.io.FileSaver;
34 import ij.process.ByteProcessor;
35 import ij.process.ImageProcessor;
36 import ij.process.FloatPolygon;
37 import ij.ImagePlus;
38 import ij.ImageStack;
39 import ij.measure.Calibration;
40 import ij.measure.ResultsTable;
42 import ini.trakem2.Project;
43 import ini.trakem2.persistence.DBObject;
44 import ini.trakem2.utils.ProjectToolbar;
45 import ini.trakem2.utils.IJError;
46 import ini.trakem2.utils.Utils;
47 import ini.trakem2.utils.M;
48 import ini.trakem2.render3d.Perimeter2D;
49 import ini.trakem2.vector.VectorString3D;
51 import java.awt.Color;
52 import java.awt.Rectangle;
53 import java.awt.Point;
54 import java.awt.Polygon;
55 import java.awt.event.MouseEvent;
56 import java.awt.event.KeyEvent;
57 import java.util.*;
58 import java.awt.geom.AffineTransform;
59 import java.awt.geom.Area;
60 import java.awt.geom.GeneralPath;
61 import java.awt.geom.NoninvertibleTransformException;
62 import java.awt.geom.PathIterator;
63 import java.awt.geom.Point2D;
64 import java.awt.image.BufferedImage;
65 import java.awt.image.DataBufferByte;
66 import java.awt.Shape;
67 import java.awt.Graphics;
68 import java.awt.Graphics2D;
69 import java.awt.Composite;
70 import java.awt.AlphaComposite;
71 import java.awt.RenderingHints;
72 import java.io.File;
74 import marchingcubes.MCTriangulator;
75 import isosurface.Triangulator;
76 import amira.AmiraMeshEncoder;
77 import amira.AmiraParameters;
79 import javax.vecmath.Point3f;
81 /** A list of brush painted areas similar to a set of labelfields in Amira.
83 * For each layer where painting has been done, there is an entry in the ht_areas HashMap that contains the layer's id as a Long, and a java.awt.geom.Area object.
84 * All Area objects are local to this AreaList's AffineTransform.
86 public class AreaList extends ZDisplayable {
88 /** Contains the table of layer ids and their associated Area object.*/
89 private HashMap ht_areas = new HashMap();
91 /** Flag to signal dynamic loading from the database for the Area of a given layer id in the ht_areas HashMap. */
92 static private Object UNLOADED = new Object();
94 /** Flag to repaint faster even if the object is selected. */
95 static private boolean brushing = false;
97 /** Paint as outlines (false) or as solid areas (true; default, with a default alpha of 0.4f).*/
98 private boolean fill_paint = true;
100 public AreaList(Project project, String title, double x, double y) {
101 super(project, title, x, y);
102 this.alpha = 0.4f;
103 addToDatabase();
106 /** Reconstruct from XML. */
107 public AreaList(Project project, long id, HashMap ht_attributes, HashMap ht_links) {
108 super(project, id, ht_attributes, ht_links);
109 // read the fill_paint
110 Object ob_data = ht_attributes.get("fill_paint");
111 try {
112 if (null != ob_data) this.fill_paint = "true".equals(((String)ob_data).trim().toLowerCase()); // fails: //Boolean.getBoolean((String)ob_data);
113 } catch (Exception e) {
114 Utils.log("AreaList: could not read fill_paint value from XML:" + e);
118 /** Reconstruct from the database. */
119 public AreaList(Project project, long id, String title, double width, double height, float alpha, boolean visible, Color color, boolean locked, ArrayList al_ul, AffineTransform at) { // al_ul contains Long() wrapping layer ids
120 super(project, id, title, locked, at, width, height);
121 this.alpha = alpha;
122 this.visible = visible;
123 this.color = color;
124 for (Iterator it = al_ul.iterator(); it.hasNext(); ) {
125 ht_areas.put(it.next(), AreaList.UNLOADED); // assumes al_ul contains only Long instances wrapping layer_id long values
129 public void paint(final Graphics2D g, final double magnification, final boolean active, final int channels, final Layer active_layer) {
130 Object ob = ht_areas.get(new Long(active_layer.getId()));
131 if (null == ob) return;
132 if (AreaList.UNLOADED == ob) {
133 ob = loadLayer(active_layer.getId());
134 if (null == ob) return;
136 final Area area = (Area)ob;
137 g.setColor(this.color);
138 //arrange transparency
139 Composite original_composite = null;
140 if (alpha != 1.0f) {
141 original_composite = g.getComposite();
142 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
145 if (fill_paint) g.fill(area.createTransformedArea(this.at));
146 else g.draw(area.createTransformedArea(this.at)); // the contour only
148 // If adding, check
149 if (null != last) {
150 try {
151 final Area tmp = last.getTmpArea();
152 if (null != tmp) {
153 if (fill_paint) g.fill(tmp.createTransformedArea(this.at));
154 else g.draw(tmp.createTransformedArea(this.at)); // won't be perfect except on mouse release
156 } catch (Exception e) {}
159 //Transparency: fix alpha composite back to original.
160 if (null != original_composite) {
161 g.setComposite(original_composite);
165 public void transformPoints(Layer layer, double dx, double dy, double rot, double xo, double yo) {
166 Utils.log("AreaList.transformPoints: not implemented yet.");
169 /** Returns the layer of lowest Z coordinate Layer where this ZDisplayable has a point in. */
170 public Layer getFirstLayer() {
171 double min_z = Double.MAX_VALUE;
172 Layer first_layer = null;
173 for (Iterator it = ht_areas.keySet().iterator(); it.hasNext(); ) {
174 Layer la = this.layer_set.getLayer(((Long)it.next()).longValue());
175 double z = la.getZ();
176 if (z < min_z) {
177 min_z = z;
178 first_layer = la;
181 return first_layer;
184 /** Returns the layer of highest Z coordinate Layer where this ZDisplayable has a point in. */
185 public Layer getLastLayer() {
186 double max_z = -Double.MAX_VALUE;
187 Layer last_layer = null;
188 for (Iterator it = ht_areas.keySet().iterator(); it.hasNext(); ) {
189 Layer la = this.layer_set.getLayer(((Long)it.next()).longValue());
190 double z = la.getZ();
191 if (z > max_z) {
192 max_z = z;
193 last_layer = la;
196 return last_layer;
197 } // I do REALLY miss Lisp macros. Writting the above two methods in a lispy way would make the java code unreadable
199 public void linkPatches() {
200 unlinkAll(Patch.class);
201 // cheap way: intersection of the patches' bounding box with the area
202 Rectangle r = new Rectangle();
203 for (Iterator it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
204 Map.Entry entry = (Map.Entry)it.next();
205 Layer la = this.layer_set.getLayer(((Long)entry.getKey()).longValue());
206 Area area = (Area)entry.getValue();
207 area = area.createTransformedArea(this.at);
208 for (Iterator dit = la.getDisplayables(Patch.class).iterator(); dit.hasNext(); ) {
209 Displayable d = (Displayable)dit.next();
210 r = d.getBoundingBox(r);
211 if (area.intersects(r)) {
212 link(d, true);
218 /** Returns whether the point x,y is contained in this object at the given Layer. */
219 public boolean contains(final Layer layer, final int x, final int y) {
220 Object ob = ht_areas.get(new Long(layer.getId()));
221 if (null == ob) return false;
222 if (AreaList.UNLOADED == ob) {
223 ob = loadLayer(layer.getId());
224 if (null == ob) return false;
226 Area area = (Area)ob;
227 if (!this.at.isIdentity()) area = area.createTransformedArea(this.at);
228 return area.contains(x, y);
231 public boolean intersects(final Layer layer, final Rectangle r) {
232 Object ob = ht_areas.get(layer.getId());
233 if (null == ob) return false;
234 if (AreaList.UNLOADED == ob) {
235 ob = loadLayer(layer.getId());
236 if (null == ob) return false;
238 final Area a = ((Area)ob).createTransformedArea(this.at);
239 return a.intersects(r.x, r.y, r.width, r.height);
242 public boolean intersects(final Layer layer, final Area area) {
243 Object ob = ht_areas.get(layer.getId());
244 if (null == ob) return false;
245 if (AreaList.UNLOADED == ob) {
246 ob = loadLayer(layer.getId());
247 if (null == ob) return false;
249 final Area a = ((Area)ob).createTransformedArea(this.at);
250 a.intersect(area);
251 final Rectangle b = a.getBounds();
252 return 0 != b.width && 0 != b.height;
255 /** Returns the bounds of this object as it shows in the given layer. */
256 public Rectangle getBounds(final Rectangle r, final Layer layer) {
257 if (null == layer) return super.getBounds(r, null);
258 final Area area = (Area)ht_areas.get(layer.getId());
259 if (null == area) {
260 if (null == r) return new Rectangle();
261 r.x = 0;
262 r.y = 0;
263 r.width = 0;
264 r.height = 0;
265 return r;
267 final Rectangle b = area.createTransformedArea(this.at).getBounds();
268 if (null == r) return b;
269 r.setBounds(b.x, b.y, b.width, b.height);
270 return r;
273 public boolean isDeletable() {
274 if (0 == ht_areas.size()) return true;
275 return false;
278 private boolean is_new = false;
280 public void mousePressed(final MouseEvent me, final int x_p_w, final int y_p_w, final double mag) {
281 final long lid = Display.getFrontLayer(this.project).getId(); // isn't this.layer pointing to the current layer always? It *should*
282 Object ob = ht_areas.get(new Long(lid));
283 Area area = null;
284 if (null == ob) {
285 area = new Area();
286 ht_areas.put(new Long(lid), area);
287 is_new = true;
288 this.width = layer_set.getLayerWidth(); // will be set properly at mouse release
289 this.height = layer_set.getLayerHeight(); // without this, the first brush slash doesn't get painted because the isOutOfRepaintingClip returns true
290 } else {
291 if (AreaList.UNLOADED == ob) {
292 ob = loadLayer(lid);
293 if (null == ob) return;
295 area = (Area)ob;
298 // transform the x_p, y_p to the local coordinates
299 int x_p = x_p_w;
300 int y_p = y_p_w;
301 if (!this.at.isIdentity()) {
302 final Point2D.Double p = inverseTransformPoint(x_p_w, y_p_w);
303 x_p = (int)p.x;
304 y_p = (int)p.y;
307 if (me.isShiftDown()) {
308 // fill in a hole if the clicked point lays within one
309 Polygon pol = null;
310 if (area.contains(x_p, y_p)) {
311 if (me.isAltDown()) {
312 // fill-remove
313 pol = M.findPath(area, x_p, y_p); // no null check, exists for sure
314 area.subtract(new Area(pol));
316 } else if (!me.isAltDown()) {
317 // fill-add
318 pol = M.findPath(area, x_p, y_p);
319 if (null != pol) {
320 area.add(new Area(pol)); // may not exist
323 if (null != pol) {
324 final Rectangle r_pol = transformRectangle(pol.getBounds());
325 Display.repaint(Display.getFrontLayer(), r_pol, 1);
326 updateInDatabase("points=" + lid);
327 } else {
328 // An area in world coords:
329 Area b = null;
330 // Try to find a hole in another visible AreaList, but fill it here
331 for (final ZDisplayable zd : Display.getFrontLayer(this.project).getParent().getZDisplayables(AreaList.class)) {
332 if ( ! zd.isVisible()) continue;
333 final AreaList ali = (AreaList) zd;
334 final Area a = ali.getArea(lid);
335 if (null == a) continue;
336 // bring point to zd space
337 final Point2D.Double p = ali.inverseTransformPoint(x_p_w, y_p_w);
338 final Polygon polygon = M.findPath(a, (int)p.x, (int)p.y);
339 if (null != polygon) {
340 // Bring polygon to world coords
341 b = new Area(polygon).createTransformedArea(ali.at);
342 break;
345 // If nothing found, try to merge all visible areas in current layer and find a hole there
346 if (null == b) {
347 final Area all = new Area(); // in world coords
348 for (final ZDisplayable zd : Display.getFrontLayer(this.project).getParent().getZDisplayables(AreaList.class)) {
349 if ( ! zd.isVisible()) continue;
350 final AreaList ali = (AreaList) zd;
351 final Area a = ali.getArea(lid);
352 if (null == a) continue;
353 all.add(a.createTransformedArea(ali.at));
355 final Polygon polygon = M.findPath(all, x_p_w, y_p_w); // in world coords
356 if (null != polygon) {
357 b = new Area(polygon);
360 if (null != b) {
361 try {
362 // Add b as local to this AreaList
363 area.add(b.createTransformedArea(this.at.createInverse()));
364 Display.repaint(Display.getFrontLayer(this.project), b.getBounds(), 1); // use b, in world coords
365 } catch (NoninvertibleTransformException nite) { IJError.print(nite); }
368 } else {
369 if (null != last) last.quit();
370 last = new BrushThread(area, mag);
371 brushing = true;
374 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) {
375 // nothing, the BrushThread handles it
376 //irrelevant//if (ProjectToolbar.getToolId() == ProjectToolbar.PEN) brushing = true;
378 public void mouseReleased(MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r) {
379 if (!brushing) {
380 // nothing changed
381 //Utils.log("AreaList mouseReleased: no brushing");
382 return;
384 brushing = false;
385 if (null != last) {
386 last.quit();
387 last = null;
389 long lid = Display.getFrontLayer(this.project).getId();
390 Object ob = ht_areas.get(new Long(lid));
391 Area area = null;
392 if (null != ob) {
393 area = (Area)ob;
395 // check if empty. If so, remove
396 Rectangle bounds = area.getBounds(); // TODO this can fail if the layer changes suddenly while painting
397 if (0 == bounds.width && 0 == bounds.height) {
398 ht_areas.remove(new Long(lid));
399 //Utils.log("removing empty area");
402 final boolean translated = calculateBoundingBox(); // will reset all areas' top-left coordinates, and update the database if necessary
403 if (translated) {
404 // update for all, since the bounding box has changed
405 updateInDatabase("all_points");
406 } else {
407 // update the points for the current layer only
408 updateInDatabase("points=" + lid);
411 // Repaint instead the last rectangle, to erase the circle
412 if (null != r_old) {
413 Display.repaint(Display.getFrontLayer(), r_old, 3, false);
414 r_old = null;
416 // repaint the navigator and snapshot
417 Display.repaint(Display.getFrontLayer(), this);
420 /** Calculate box, make this width,height be that of the box, and translate all areas to fit in. @param lid is the currently active Layer. */ //This is the only road to sanity for ZDisplayable objects.
421 public boolean calculateBoundingBox() {
422 // forget it if this has been done once already, for at the moment it would work only for translations, not any other types of transforms. TODO: need to fix this somehow, generates repainting problems.
423 //if (this.at.getType() != AffineTransform.TYPE_TRANSLATION) return false; // meaning, there's more bits in the type than just the translation
424 // check preconditions
425 if (0 == ht_areas.size()) return false;
426 Area[] area = new Area[ht_areas.size()];
427 Map.Entry[] entry = new Map.Entry[area.length];
428 ht_areas.entrySet().toArray(entry);
429 ht_areas.values().toArray(area);
430 Rectangle[] b = new Rectangle[area.length];
431 Rectangle box = null;
432 for (int i=0; i<area.length; i++) {
433 b[i] = area[i].getBounds();
434 if (null == box) box = (Rectangle)b[i].clone();
435 else box.add(b[i]);
437 if (null == box) return false; // empty AreaList
438 final AffineTransform atb = new AffineTransform();
439 atb.translate(-box.x, -box.y); // make local to overall box, so that box starts now at 0,0
440 for (int i=0; i<area.length; i++) {
441 entry[i].setValue(area[i].createTransformedArea(atb));
443 //this.translate(box.x, box.y);
444 this.at.translate(box.x, box.y);
445 this.width = box.width;
446 this.height = box.height;
447 updateInDatabase("transform+dimensions");
448 if (null != layer_set) layer_set.updateBucket(this);
449 if (0 != box.x || 0 != box.y) {
450 return true;
452 return false;
455 private BrushThread last = null;
456 static private Rectangle r_old = null;
458 /** Modeled after the ij.gui.RoiBrush class from ImageJ. */
459 private class BrushThread extends Thread {
460 /** The area to paint into when done or when removing. */
461 final private Area target_area;
462 /** The temporary area to paint to, which is only different than target_area when adding. */
463 final private Area area;
464 /** The list of all painted points. */
465 private final ArrayList<Point> points = new ArrayList<Point>();
466 /** The last point on which a paint event was done. */
467 private Point previous_p = null;
468 private boolean paint = true;
469 private int brush_size; // the diameter
470 private Area brush;
471 final private int leftClick=16, alt=9;
472 final private DisplayCanvas dc = Display.getFront().getCanvas();
473 final private int flags = dc.getModifiers();
474 private boolean adding = (0 == (flags & alt));
476 BrushThread(Area area, double mag) {
477 super("BrushThread");
478 setPriority(Thread.NORM_PRIORITY);
479 // if adding areas, make it be a copy, to be added on mouse release
480 // (In this way, the receiving Area is small and can be operated on fast)
481 if (adding) {
482 this.target_area = area;
483 this.area = new Area();
484 } else {
485 this.target_area = area;
486 this.area = area;
489 brush_size = ProjectToolbar.getBrushSize();
490 brush = makeBrush(brush_size, mag);
491 if (null == brush) return;
492 start();
494 final void quit() {
495 this.paint = false;
496 // Make interpolated points effect add or subtract operations
497 synchronized (this) {
498 if (points.size() < 2) {
499 // merge the temporary Area, if any, with the general one
500 if (adding) this.target_area.add(area);
501 return;
504 try {
506 // paint the regions between points
507 // A cheap way would be to just make a rectangle between both points, with thickess radius.
508 // A better, expensive way is to fit a spline first, then add each one as a circle.
509 // The spline way is wasteful, but way more precise and beautiful. Since there's only one repaint, it's not excessively slow.
510 int[] xp = new int[points.size()];
511 int[] yp = new int[xp.length];
512 int j = 0;
513 for (final Point p : points) {
514 xp[j] = p.x;
515 yp[j] = p.y;
516 j++;
518 points.clear();
520 PolygonRoi proi = new PolygonRoi(xp, yp, xp.length, Roi.POLYLINE);
521 proi.fitSpline();
522 FloatPolygon fp = proi.getFloatPolygon();
523 proi = null;
525 double[] xpd = new double[fp.npoints];
526 double[] ypd = new double[fp.npoints];
527 // Fails: fp contains float[], which for some reason cannot be copied into double[]
528 //System.arraycopy(fp.xpoints, 0, xpd, 0, xpd.length);
529 //System.arraycopy(fp.ypoints, 0, ypd, 0, ypd.length);
530 for (int i=0; i<xpd.length; i++) {
531 xpd[i] = fp.xpoints[i];
532 ypd[i] = fp.ypoints[i];
534 fp = null;
536 try {
537 // VectorString2D resampling doesn't work
538 VectorString3D vs = new VectorString3D(xpd, ypd, new double[xpd.length], false);
539 double delta = ((double)brush_size) / 10;
540 if (delta < 1) delta = 1;
541 vs.resample(delta);
542 xpd = vs.getPoints(0);
543 ypd = vs.getPoints(1);
544 vs = null;
545 } catch (Exception e) { IJError.print(e); }
548 final AffineTransform atb = new AffineTransform();
550 final AffineTransform inv_at = at.createInverse();
552 if (adding) {
553 adding = false;
554 for (int i=0; i<xpd.length; i++) {
555 atb.setToTranslation((int)xpd[i], (int)ypd[i]); // always integers
556 atb.preConcatenate(inv_at);
557 area.add(slashInInts(brush.createTransformedArea(atb)));
559 this.target_area.add(area);
560 } else {
561 // subtract
562 for (int i=0; i<xpd.length; i++) {
563 atb.setToTranslation((int)xpd[i], (int)ypd[i]); // always integers
564 atb.preConcatenate(inv_at);
565 target_area.subtract(slashInInts(brush.createTransformedArea(atb)));
569 } catch (Exception ee) {
570 IJError.print(ee);
574 final Area getTmpArea() {
575 if (area != target_area) return area;
576 return null;
578 /** For best smoothness, each mouse dragged event should be captured!*/
579 public void run() {
580 // create brush
581 Point p;
582 final AffineTransform atb = new AffineTransform();
583 while (paint) {
584 // detect mouse up
585 if (0 == (flags & leftClick)) {
586 quit();
587 return;
589 p = dc.getCursorLoc(); // as offscreen coords
590 if (p.equals(previous_p) /*|| (null != previous_p && p.distance(previous_p) < brush_size/5) */) {
591 try { Thread.sleep(1); } catch (InterruptedException ie) {}
592 continue;
594 // bring to offscreen position of the mouse
595 atb.translate(p.x, p.y);
596 // capture bounds while still in offscreen coordinates
597 final Rectangle r = new Rectangle();
598 final Area slash = createSlash(atb, r);
599 if(null == slash) continue;
601 if (0 == (flags & alt)) {
602 // no modifiers, just add
603 area.add(slash);
604 } else {
605 // with alt down, substract
606 area.subtract(slash);
608 points.add(p);
609 previous_p = p;
611 final Rectangle copy = (Rectangle)r.clone();
612 if (null != r_old) r.add(r_old);
613 r_old = copy;
615 Display.repaint(Display.getFrontLayer(), 3, r, false, false); // repaint only the last added slash
617 // reset
618 atb.setToIdentity();
622 /** Sets the bounds of the created slash, in offscreen coords, to r if r is not null. */
623 private Area createSlash(final AffineTransform atb, final Rectangle r) {
624 Area slash = brush.createTransformedArea(atb); // + int transform, no problem
625 if (null != r) r.setRect(slash.getBounds());
626 // bring to the current transform, if any
627 if (!at.isIdentity()) {
628 try {
629 slash = slash.createTransformedArea(at.createInverse());
630 } catch (NoninvertibleTransformException nite) {
631 IJError.print(nite);
632 return null;
635 // avoid problems with floating-point points, for example inability to properly fill areas or delete them.
636 return slashInInts(slash);
639 private final Area slashInInts(final Area area) {
640 int[] x = new int[400];
641 int[] y = new int[400];
642 int next = 0;
643 for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) {
644 if (x.length == next) {
645 int[] x2 = new int[x.length + 200];
646 int[] y2 = new int[y.length + 200];
647 System.arraycopy(x, 0, x2, 0, x.length);
648 System.arraycopy(y, 0, y2, 0, y.length);
649 x = x2;
650 y = y2;
652 final float[] coords = new float[6];
653 int seg_type = pit.currentSegment(coords);
654 switch (seg_type) {
655 case PathIterator.SEG_MOVETO:
656 case PathIterator.SEG_LINETO:
657 x[next] = (int)coords[0];
658 y[next] = (int)coords[1];
659 break;
660 case PathIterator.SEG_CLOSE:
661 break;
662 default:
663 Utils.log2("WARNING: AreaList.slashInInts unhandled seg type.");
664 break;
666 pit.next();
667 if (pit.isDone()) break; // the loop
668 next++;
670 // resize back (now next is the length):
671 if (x.length == next) {
672 int[] x2 = new int[next];
673 int[] y2 = new int[next];
674 System.arraycopy(x, 0, x2, 0, next);
675 System.arraycopy(y, 0, y2, 0, next);
676 x = x2;
677 y = y2;
679 return new Area(new Polygon(x, y, next));
682 /** This method could get tones of improvement, which should be pumped upstream into ImageJ's RoiBrush class which is creating it at every while(true) {} iteration!!!
683 * The returned area has its coordinates centered around 0,0
685 private Area makeBrush(int diameter, double mag) {
686 if (diameter < 1) return null;
687 if (mag >= 1) return new Area(new OvalRoi(-diameter/2, -diameter/2, diameter, diameter).getPolygon());
688 // else, create a smaller brush and transform it up, i.e. less precise, less points to store -but precision matches what the eye sees, and allows for much better storage -less points.
689 int screen_diameter = (int)(diameter * mag);
690 if (0 == screen_diameter) return null; // can't paint at this mag with this diameter
692 Area brush = new Area(new OvalRoi(-screen_diameter/2, -screen_diameter/2, screen_diameter, screen_diameter).getPolygon());
693 // scale to world coordinates
694 AffineTransform at = new AffineTransform();
695 at.scale(1/mag, 1/mag);
696 return brush.createTransformedArea(at);
699 // smooth out edges
701 Polygon pol = new OvalRoi(-diameter/2, -diameter/2, diameter, diameter).getPolygon();
702 Polygon pol2 = new Polygon();
703 // cheap and fast: skip every other point, since all will be square angles
704 for (int i=0; i<pol.npoints; i+=2) {
705 pol2.addPoint(pol.xpoints[i], pol.ypoints[i]);
707 return new Area(pol2);
708 // the above works nice, but then the fill and fill-remove don't work properly (there are traces in the edges)
709 // Needs a workround: before adding/substracting, enlarge the polygon to have square edges
714 static public void exportDTD(StringBuffer sb_header, HashSet hs, String indent) {
715 String type = "t2_area_list";
716 if (hs.contains(type)) return;
717 hs.add(type);
718 sb_header.append(indent).append("<!ELEMENT t2_area_list (").append(Displayable.commonDTDChildren()).append(",t2_area)>\n");
719 Displayable.exportDTD(type, sb_header, hs, indent); // all ATTLIST of a Displayable
720 sb_header.append(indent).append("<!ATTLIST t2_area_list fill_paint NMTOKEN #REQUIRED>\n");
721 sb_header.append(indent).append("<!ELEMENT t2_area (t2_path)>\n")
722 .append(indent).append("<!ATTLIST t2_area layer_id NMTOKEN #REQUIRED>\n")
723 .append(indent).append("<!ELEMENT t2_path EMPTY>\n")
724 .append(indent).append("<!ATTLIST t2_path d NMTOKEN #REQUIRED>\n")
728 public void exportXML(StringBuffer sb_body, String indent, Object any) {
729 sb_body.append(indent).append("<t2_area_list\n");
730 final String in = indent + "\t";
731 super.exportXML(sb_body, in, any);
732 sb_body.append(in).append("fill_paint=\"").append(fill_paint).append("\"\n");
733 String[] RGB = Utils.getHexRGBColor(color);
734 sb_body.append(in).append("style=\"stroke:none;fill-opacity:").append(alpha).append(";fill:#").append(RGB[0]).append(RGB[1]).append(RGB[2]).append(";\"\n");
735 sb_body.append(indent).append(">\n");
736 for (Iterator it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
737 Map.Entry entry = (Map.Entry)it.next();
738 long lid = ((Long)entry.getKey()).longValue();
739 Area area = (Area)entry.getValue();
740 sb_body.append(in).append("<t2_area layer_id=\"").append(lid).append("\">\n");
741 exportArea(sb_body, in + "\t", area);
742 sb_body.append(in).append("</t2_area>\n");
744 super.restXML(sb_body, in, any);
745 sb_body.append(indent).append("</t2_area_list>\n");
748 /** Exports the given area as a list of SVG path elements with integers only. Only reads SEG_MOVETO, SEG_LINETO and SEG_CLOSE elements, all others ignored (but could be just as easily saved in the SVG path). */
749 private void exportArea(StringBuffer sb, String indent, Area area) {
750 // I could add detectors for straight lines and thus avoid saving so many points.
751 for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) {
752 float[] coords = new float[6];
753 int seg_type = pit.currentSegment(coords);
754 int x0=0, y0=0;
755 switch (seg_type) {
756 case PathIterator.SEG_MOVETO:
757 //Utils.log2("SEG_MOVETO: " + coords[0] + "," + coords[1]); // one point
758 x0 = (int)coords[0];
759 y0 = (int)coords[1];
760 sb.append(indent).append("<t2_path d=\"M ").append(x0).append(" ").append(y0);
761 break;
762 case PathIterator.SEG_LINETO:
763 //Utils.log2("SEG_LINETO: " + coords[0] + "," + coords[1]); // one point
764 sb.append(" L ").append((int)coords[0]).append(" ").append((int)coords[1]);
765 break;
766 case PathIterator.SEG_CLOSE:
767 //Utils.log2("SEG_CLOSE");
768 // make a line to the first point
769 //sb.append(" L ").append(x0).append(" ").append(y0);
770 sb.append(" z\" />\n");
771 break;
772 default:
773 Utils.log2("WARNING: AreaList.exportArea unhandled seg type.");
774 break;
776 pit.next();
777 if (pit.isDone()) {
778 //Utils.log2("finishing");
779 return;
783 /** Exports the given area as a list of SVG path elements with integers only. Only reads SEG_MOVETO, SEG_LINETO and SEG_CLOSE elements, all others ignored (but could be just as easily saved in the SVG path). */
784 private void exportAreaT2(final StringBuffer sb, final String indent, final Area area) {
785 // I could add detectors for straight lines and thus avoid saving so many points.
786 for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) {
787 float[] coords = new float[6];
788 int seg_type = pit.currentSegment(coords);
789 int x0=0, y0=0;
790 switch (seg_type) {
791 case PathIterator.SEG_MOVETO:
792 x0 = (int)coords[0];
793 y0 = (int)coords[1];
794 sb.append(indent).append("(path '(M ").append(x0).append(' ').append(y0);
795 break;
796 case PathIterator.SEG_LINETO:
797 sb.append(" L ").append((int)coords[0]).append(' ').append((int)coords[1]);
798 break;
799 case PathIterator.SEG_CLOSE:
800 // no need to make a line to the first point
801 sb.append(" z))\n");
802 break;
803 default:
804 Utils.log2("WARNING: AreaList.exportArea unhandled seg type.");
805 break;
807 pit.next();
808 if (pit.isDone()) {
809 return;
814 /** Returns an ArrayList of ArrayList of Point as value with all paths for the Area of the given layer_id. */
815 public ArrayList getPaths(long layer_id) {
816 Object ob = ht_areas.get(new Long(layer_id));
817 if (null == ob) return null;
818 if (AreaList.UNLOADED == ob) {
819 ob = loadLayer(layer_id);
820 if (null == ob) return null;
822 Area area = (Area)ob;
823 ArrayList al_paths = new ArrayList();
824 ArrayList al_points = null;
825 for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) {
826 float[] coords = new float[6];
827 int seg_type = pit.currentSegment(coords);
828 switch (seg_type) {
829 case PathIterator.SEG_MOVETO:
830 al_points = new ArrayList();
831 al_points.add(new Point((int)coords[0], (int)coords[1]));
832 break;
833 case PathIterator.SEG_LINETO:
834 al_points.add(new Point((int)coords[0], (int)coords[1]));
835 break;
836 case PathIterator.SEG_CLOSE:
837 al_paths.add(al_points);
838 al_points = null;
839 break;
840 default:
841 Utils.log2("WARNING: AreaList.getPaths() unhandled seg type.");
842 break;
844 pit.next();
845 if (pit.isDone()) {
846 break;
849 return al_paths;
852 /** Returns a table of Long layer ids versus the ArrayList that getPaths(long) returns for it.*/
853 public HashMap getAllPaths() {
854 HashMap ht = new HashMap();
855 for (Iterator it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
856 Map.Entry entry = (Map.Entry)it.next();
857 ht.put(entry.getKey(), getPaths(((Long)entry.getKey()).longValue()));
859 return ht;
862 /** These methods below prepended with double-underscore to mean: they are public, but should not be used except by the TMLHandler. I could put a caller detector but parsing XML code doesn't need any more overhead.*/
864 /** For reconstruction from XML. */
865 public void __startReconstructing(long lid) {
866 ht_areas.put("CURRENT", new Long(lid));
867 ht_areas.put(new Long(lid), new GeneralPath(GeneralPath.WIND_EVEN_ODD));
869 /** For reconstruction from XML. */
870 public void __addPath(String svg_path) {
871 GeneralPath gp = (GeneralPath)ht_areas.get(ht_areas.get("CURRENT"));
872 svg_path = svg_path.trim();
873 while (-1 != svg_path.indexOf(" ")) {
874 svg_path = svg_path.replaceAll(" "," "); // make all spaces be single
876 final char[] data = new char[svg_path.length()];
877 svg_path.getChars(0, data.length, data, 0);
878 parse(gp, data);
881 /** Assumes first char is 'M' and last char is a 'z'*/
882 private void parse(final GeneralPath gp, final char[] data) {
883 if ('z' != data[data.length-1]) {
884 Utils.log("AreaList: no closing z, ignoring sub path");
885 return;
887 data[data.length-1] = 'L'; // replacing the closing z for an L, since we read backwards
888 final int[] xy = new int[2];
889 int i_L = -1;
890 // find first L
891 for (int i=0; i<data.length; i++) {
892 if ('L' == data[i]) {
893 i_L = i;
894 break;
897 readXY(data, i_L, xy);
898 final int x0 = xy[0];
899 final int y0 = xy[1];
900 gp.moveTo(x0, y0);
901 int first = i_L+1;
902 while (-1 != (first = readXY(data, first, xy))) {
903 gp.lineTo(xy[0], xy[1]);
906 // close loop
907 gp.lineTo(x0, y0); //TODO unnecessary?
908 gp.closePath();
911 /** Assumes all read chars will be digits except for the separator (single white space char), and won't fail (but generate ugly results) when any char is not a digit. */
912 private final int readXY(final char[] data, int first, final int[] xy) { // final method: inline
913 if (first >= data.length) return -1;
914 int last = first;
915 char c = data[first];
916 while ('L' != c) {
917 last++;
918 if (data.length == last) return -1;
919 c = data[last];
921 first = last +2; // the first digit position after the found L, which will be the next first.
923 // skip the L and the white space separating <y> and L
924 last -= 2;
925 if (last < 0) return -1;
926 c = data[last];
928 // the 'y'
929 xy[1] = 0;
930 int pos = 1;
931 while (' ' != c) {
932 xy[1] += (((int)c) -48) * pos; // digit zero is char with int value 48
933 last--;
934 c = data[last];
935 pos *= 10;
938 // skip separating space
939 last--;
941 // the 'x'
942 c = data[last];
943 pos = 1;
944 xy[0] = 0;
945 while (' ' != c) {
946 xy[0] += (((int)c) -48) * pos;
947 last--;
948 c = data[last];
949 pos *= 10;
951 return first;
953 /** For reconstruction from XML. */
954 public void __endReconstructing() {
955 Object ob = ht_areas.get("CURRENT");
956 if (null != ob) {
957 GeneralPath gp = (GeneralPath)ht_areas.get((Long)ob);
958 ht_areas.put(ob, new Area(gp));
959 ht_areas.remove("CURRENT");
963 public void fillHoles(final Layer la) {
964 Object o = ht_areas.get(la.getId());
965 if (UNLOADED == o) o = loadLayer(la.getId());
966 if (null == o) return;
967 Area area = (Area) o;
969 Polygon pol = new Polygon();
970 for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) {
971 float[] coords = new float[6];
972 int seg_type = pit.currentSegment(coords);
973 switch (seg_type) {
974 case PathIterator.SEG_MOVETO:
975 case PathIterator.SEG_LINETO:
976 pol.addPoint((int)coords[0], (int)coords[1]);
977 break;
978 case PathIterator.SEG_CLOSE:
979 area.add(new Area(pol));
980 // prepare next:
981 pol = new Polygon();
982 break;
983 default:
984 Utils.log2("WARNING: unhandled seg type.");
985 break;
987 pit.next();
988 if (pit.isDone()) {
989 break;
994 public boolean paintsAt(final Layer layer) {
995 if (!super.paintsAt(layer)) return false;
996 return null != ht_areas.get(new Long(layer.getId()));
999 /** Dynamic loading from the database. */
1000 private Area loadLayer(final long layer_id) {
1001 Area area = project.getLoader().fetchArea(this.id, layer_id);
1002 if (null == area) return null;
1003 ht_areas.put(new Long(layer_id), area);
1004 return area;
1007 public void adjustProperties() {
1008 GenericDialog gd = makeAdjustPropertiesDialog(); // in superclass
1009 gd.addCheckbox("Paint as outlines", !fill_paint);
1010 gd.addCheckbox("Apply paint mode to all AreaLists", false);
1011 gd.showDialog();
1012 if (gd.wasCanceled()) return;
1013 // superclass processing
1014 final Displayable.DoEdit prev = processAdjustPropertiesDialog(gd);
1015 // local proccesing
1016 final boolean fp = !gd.getNextBoolean();
1017 final boolean to_all = gd.getNextBoolean();
1018 if (to_all) {
1019 for (Iterator it = this.layer_set.getZDisplayables().iterator(); it.hasNext(); ) {
1020 Object ob = it.next();
1021 if (ob instanceof AreaList) {
1022 AreaList ali = (AreaList)ob;
1023 ali.fill_paint = fp;
1024 ali.updateInDatabase("fill_paint");
1025 Display.repaint(this.layer_set, ali, 2);
1028 } else {
1029 if (this.fill_paint != fp) {
1030 prev.add("fill_paint", fp);
1031 this.fill_paint = fp;
1032 updateInDatabase("fill_paint");
1036 // Add current step, with the same modified keys
1037 DoEdit current = new DoEdit(this).init(prev);
1038 if (isLinked()) current.add(new Displayable.DoTransforms().addAll(getLinkedGroup(null)));
1039 getLayerSet().addEditStep(current);
1042 public boolean isFillPaint() { return this.fill_paint; }
1044 /** Merge all arealists contained in the ArrayList to the first one found, and remove the others from the project, and only if they belong to the same LayerSet. Returns the merged AreaList object. */
1045 static public AreaList merge(final ArrayList al) {
1046 AreaList base = null;
1047 final ArrayList list = (ArrayList)al.clone();
1048 for (Iterator it = list.iterator(); it.hasNext(); ) {
1049 Object ob = it.next();
1050 if (ob.getClass() != AreaList.class) it.remove();
1051 if (null == base) {
1052 base = (AreaList)ob;
1053 if (null == base.layer_set) {
1054 Utils.log2("AreaList.merge: null LayerSet for base AreaList.");
1055 return null;
1057 it.remove();
1059 // check that it belongs to the same layer set as the base
1060 AreaList ali = (AreaList)ob;
1061 if (base.layer_set != ali.layer_set) it.remove();
1063 if (list.size() < 1) return null; // nothing to fuse
1064 if (!Utils.check("Merging AreaList has no undo. Continue?")) return null;
1065 for (Iterator it = list.iterator(); it.hasNext(); ) {
1066 // add to base
1067 AreaList ali = (AreaList)it.next();
1068 base.add(ali);
1069 // remove from project
1070 ali.project.removeProjectThing(ali, false, true, 1); // the node from the Project Tree
1071 Utils.log2("Merged AreaList " + ali + " to base " + base);
1073 // update
1074 base.calculateBoundingBox();
1075 // relink
1076 base.linkPatches();
1078 return base;
1081 /** For each area that ali contains, add it to the corresponding area here.*/
1082 private void add(AreaList ali) {
1083 for (Iterator it = ali.ht_areas.entrySet().iterator(); it.hasNext(); ) {
1084 Map.Entry entry = (Map.Entry)it.next();
1085 Object ob_area = entry.getValue();
1086 long lid = ((Long)entry.getKey()).longValue();
1087 if (UNLOADED == ob_area) ob_area = ali.loadLayer(lid);
1088 Area area = (Area)ob_area;
1089 area = area.createTransformedArea(ali.at);
1090 // now need to inverse transform it by this.at
1091 try {
1092 area = area.createTransformedArea(this.at.createInverse());
1093 } catch (NoninvertibleTransformException nte) {
1094 IJError.print(nte);
1095 // do what?
1097 Object this_area = this.ht_areas.get(entry.getKey());
1098 if (UNLOADED == this_area) { this_area = loadLayer(lid); }
1099 if (null == this_area) this.ht_areas.put(entry.getKey(), (Area)area.clone());
1100 else ((Area)this_area).add(area);
1101 updateInDatabase("points=" + ((Long)entry.getKey()).intValue());
1105 /** How many layers does this object paint to. */
1106 public int getNAreas() { return ht_areas.size(); }
1108 public Area getArea(Layer la) {
1109 if (null == la) return null;
1110 return getArea(la.getId());
1112 public Area getArea(long layer_id) {
1113 Object ob = ht_areas.get(new Long(layer_id));
1114 if (null != ob) {
1115 if (UNLOADED == ob) ob = loadLayer(layer_id);
1116 return (Area)ob;
1118 return null;
1123 /** Performs a deep copy of this object, without the links. */
1124 public Displayable clone(final Project pr, final boolean copy_id) {
1125 final ArrayList al_ul = new ArrayList();
1126 for (Iterator it = ht_areas.keySet().iterator(); it.hasNext(); ) { // TODO WARNING the layer ids are wrong if the project is different or copy_id is false! Should lookup closest layer by Z ...
1127 al_ul.add(new Long(((Long)it.next()).longValue())); // clones of the Long that wrap layer ids
1129 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
1130 final AreaList copy = new AreaList(pr, nid, null != title ? title.toString() : null, width, height, alpha, this.visible, new Color(color.getRed(), color.getGreen(), color.getBlue()), this.visible, al_ul, (AffineTransform)this.at.clone());
1131 for (Iterator it = copy.ht_areas.entrySet().iterator(); it.hasNext(); ) {
1132 Map.Entry entry = (Map.Entry)it.next();
1133 entry.setValue(((Area)this.ht_areas.get(entry.getKey())).clone());
1135 return copy;
1138 /** Will make the assumption that all layers have the same thickness as the first one found.
1139 * @param scale The scaling of the entire universe, to limit the overall box
1140 * @param resample The optimization parameter for marching cubes (i.e. a value of 2 will scale down to half, then apply marching cubes, then scale up by 2 the vertices coordinates).
1141 * @return The List of triangles involved, specified as three consecutive vertices. A list of Point3f vertices.
1143 public List generateTriangles(final double scale, final int resample_) {
1144 // in the LayerSet, layers are ordered by Z already.
1145 try {
1146 final int resample;
1147 if (resample_ <=0 ) {
1148 resample = 1;
1149 Utils.log2("Fixing zero or negative resampling value to 1.");
1150 } else resample = resample_;
1152 int n = getNAreas();
1153 if (0 == n) return null;
1154 final Rectangle r = getBoundingBox();
1155 // remove translation from a copy of this Displayable's AffineTransform
1156 final AffineTransform at_translate = new AffineTransform();
1157 at_translate.translate(-r.x, -r.y);
1158 final AffineTransform at2 = (AffineTransform)this.at.clone();
1159 at2.preConcatenate(at_translate);
1160 // incorporate resampling scaling into the transform
1161 final AffineTransform atK = new AffineTransform();
1162 //Utils.log("resample: " + resample + " scale: " + scale);
1163 final double K = (1.0 / resample) * scale; // 'scale' is there to limit gigantic universes
1164 atK.scale(K, K);
1165 at2.preConcatenate(atK);
1167 final Calibration cal = layer_set.getCalibrationCopy();
1170 ImageStack stack = null;
1171 final int w = (int)Math.ceil(r.width * K);
1172 final int h = (int)Math.ceil(r.height * K);
1174 // For the thresholding after painting an scaled-down area into an image:
1175 final int threshold;
1176 if (K > 0.8) threshold = 200;
1177 else if (K > 0.5) threshold = 128;
1178 else if (K > 0.3) threshold = 75; // 75 gives upper 70% of 255 range. It's better to blow up a bit, since resampling down makes marching cubes undercut the mesh.
1179 else threshold = 40;
1181 Layer first_layer = null;
1183 final HashSet<Layer> empty_layers = new HashSet<Layer>();
1185 for (final Layer la : layer_set.getLayers()) { // layers sorted by Z ASC
1186 if (0 == n) break; // no more areas to paint
1187 final Area area = getArea(la);
1188 if (null != area) {
1189 if (null == stack) {
1190 stack = new ImageStack(w, h);
1191 first_layer = la;
1193 project.getLoader().releaseToFit(w, h, ImagePlus.GRAY8, 3);
1194 // must be a new image, for pixel array is shared with BufferedImage and ByteProcessor in java 1.6
1195 final BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1196 final Graphics2D g = bi.createGraphics();
1197 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
1198 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1199 g.setColor(Color.white);
1200 g.fill(area.createTransformedArea(at2));
1201 final ByteProcessor bp = new ByteProcessor(bi);
1202 bp.threshold(threshold);
1203 stack.addSlice(Double.toString(la.getZ()), bp);
1204 bi.flush();
1206 n--;
1208 } else if (null != stack) {
1209 // add a black slice
1210 stack.addSlice(la.getZ() + "", new ByteProcessor(w, h));
1211 empty_layers.add(la);
1215 // zero-pad stack
1216 // No need anymore: MCTriangulator does it on its own now
1217 stack = zeroPad(stack);
1219 // Still, the MCTriangulator does NOT zero pad properly
1221 final ImagePlus imp = new ImagePlus("", stack);
1222 imp.getCalibration().pixelWidth = 1; // ensure all set to 1.
1223 imp.getCalibration().pixelHeight = 1;
1224 imp.getCalibration().pixelDepth = 1;
1226 // Now marching cubes
1227 final Triangulator tri = new MCTriangulator();
1228 final List list = tri.getTriangles(imp, 0, new boolean[]{true, true, true}, 1);
1231 // The list of triangles has coordinates:
1232 // - in x,y: in pixels, scaled by K = (1 / resample) * scale,
1233 // translated by r.x, r.y (the top-left coordinate of this AreaList bounding box)
1234 // - in z: in stack slice indices
1236 // So all x,y,z must be corrected in x,y and z of the proper layer
1239 final double offset = first_layer.getZ();
1240 final int i_first_layer = layer_set.indexOf(first_layer);
1242 // The x,y translation to correct each point by:
1243 final float dx = (float)(r.x * scale * cal.pixelWidth);
1244 final float dy = (float)(r.y * scale * cal.pixelHeight);
1246 // Correct x,y by resampling and calibration, but not scale (TODO this hints that calibration in 3D is wrong: should be divided by the scale! May affect all Displayable types)
1247 final float rsw = (float)(resample * cal.pixelWidth); // scale is already in the pixel coordinates
1248 final float rsh = (float)(resample * cal.pixelHeight);
1249 final double sz = scale * cal.pixelWidth; // no resampling in Z. and Uses pixelWidth, not pixelDepth.
1252 int slice_index = 0;
1255 /* // debug:
1256 // which p.z types exist?
1257 final TreeSet<Float> ts = new TreeSet<Float>();
1258 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1259 ts.add(((Point3f)it.next()).z);
1261 for (final Float pz : ts) Utils.log2("A z: " + pz);
1263 Utils.log2("Number of slices: " + imp.getNSlices());
1266 // Fix all points:
1268 //if (i_first_layer > 0)
1269 // fix3DPoints(list, layer_set.previous(layer_set.getLayer(i_first_layer -1)), -1, dx, dy, rsw, rsh, sz);
1270 fix3DPoints(list, layer_set.previous(layer_set.getLayer(i_first_layer)), 0, dx, dy, rsw, rsh, sz);
1272 for (final Layer la : layer_set.getLayers().subList(i_first_layer, i_first_layer + imp.getNSlices() -2)) { // -2: it's padded
1274 //Utils.log2("handling slice_index: " + slice_index);
1276 // If layer is empty, continue
1277 if (empty_layers.contains(la)) {
1278 slice_index++;
1279 continue;
1282 fix3DPoints(list, la, slice_index + 1, dx, dy, rsw, rsh, sz); // +1 because of padding
1284 //Utils.log2("processed slice index " + slice_index + " for layer " + la);
1286 slice_index++;
1289 // The last set of vertices to process:
1290 // Find all pixels that belong to the layer, and transform them back:
1291 //final Layer last = layer_set.getLayer(layer_index -1);
1292 //fixLast3DPoints(list, last.getZ() + last.getThickness(), last.getThickness(), layer_index +1, dx, dy, rsw, rsh, sz);
1293 try {
1294 // Do the last layer again, capturing from slice_index+1 to +2, since the last layer has two Z planes in which it has pixels:
1295 Layer la = layer_set.getLayer(i_first_layer + slice_index -1);
1296 fix3DPoints(list, la, slice_index +1, dx, dy, rsw, rsh, sz);
1297 } catch (Exception ee) {
1298 IJError.print(ee);
1301 return list;
1303 } catch (Exception e) {
1304 e.printStackTrace();
1306 return null;
1309 private final void fix3DPoints(final List list, final Layer la, final int layer_index, final float dx, final float dy, final float rsw, final float rsh, final double sz) {
1310 final double la_z = la.getZ();
1311 final double la_thickness = la.getThickness();
1312 // Find all pixels that belong to the layer, and transform them back:
1313 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1314 final Point3f p = (Point3f)it.next();
1315 if (p.z >= layer_index && p.z < layer_index + 1) {
1316 // correct pixel position:
1317 // -- The '-1' corrects for zero padding
1318 // -- The 'rsw','rsh' scales back to LayerSet coords
1319 // -- The 'dx','dy' translates back to this AreaList bounding box
1320 p.x = (p.x -1) * rsw + dx;
1321 p.y = (p.y -1) * rsh + dy;
1323 // The Z is more complicated: the Z of the layer, scaled relative to the layer thickness
1324 // -- 'offset' is the Z of the first layer, corresponding to the layer that contributed to the first stack slice.
1325 p.z = (float)((la_z + la_thickness * (p.z - layer_index)) * sz); // using pixelWidth, not pixelDepth!
1331 private final void fixLast3DPoints(final List list, final double la_z, final double la_thickness, final int layer_index, final float dx, final float dy, final float rsw, final float rsh, final double sz) {
1332 // Find all pixels that belong to the layer, and transform them back:
1333 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1334 final Point3f p = (Point3f)it.next();
1335 if (p.z >= layer_index) {
1336 // correct pixel position:
1337 // -- The '-1' corrects for zero padding
1338 // -- The 'rsw','rsh' scales back to LayerSet coords
1339 // -- The 'dx','dy' translates back to this AreaList bounding box
1340 p.x = (p.x -1) * rsw + dx;
1341 p.y = (p.y -1) * rsh + dy;
1343 // The Z is more complicated: the Z of the layer, scaled relative to the layer thickness
1344 // -- 'offset' is the Z of the first layer, corresponding to the layer that contributed to the first stack slice.
1345 p.z = (float)((la_z + la_thickness * (p.z - layer_index)) * sz); // unsing pixelWidth, not pixelDepth!
1351 static private ImageStack zeroPad(final ImageStack stack) {
1352 int w = stack.getWidth();
1353 int h = stack.getHeight();
1354 // enlarge all processors
1355 ImageStack st = new ImageStack(w+2, h+2);
1356 for (int i=1; i<=stack.getSize(); i++) {
1357 ImageProcessor ip = new ByteProcessor(w+2, h+2);
1358 ip.insert(stack.getProcessor(i), 1, 1);
1359 st.addSlice(Integer.toString(i), ip);
1361 ByteProcessor bp = new ByteProcessor(w+2, h+2);
1362 // insert slice at 0
1363 st.addSlice("0", bp, 0);
1364 // append slice at the end
1365 st.addSlice(Integer.toString(stack.getSize()+1), bp);
1367 return st;
1370 /** Directly place an Area for the specified layer. Keep in mind it will be added in this AreaList coordinate space, not the overall LayerSet coordinate space. Does not make it local, you should call calculateBoundingBox() after setting an area. */
1371 public void setArea(final long layer_id, final Area area) {
1372 if (null == area) return;
1373 ht_areas.put(layer_id, area);
1374 updateInDatabase("points=" + layer_id);
1377 /** Add a copy of an Area object to the existing, if any, area object at Layer with layer_id as given, or if not existing, just set the copy as it. The area is expected in this AreaList coordinate space. Does not make it local, you should call calculateBoundingBox when done. */
1378 public void addArea(final long layer_id, final Area area) {
1379 if (null == area) return;
1380 Area a = getArea(layer_id);
1381 if (null == a) ht_areas.put(layer_id, new Area(area));
1382 else a.add(area);
1383 updateInDatabase("points=" + layer_id);
1386 /** Adds the given ROI, which is expected in world/LayerSet coordinates, to the area present at Layer with id layer_id, or set it if none present yet. */
1387 public void add(final long layer_id, final ShapeRoi roi) throws NoninvertibleTransformException{
1388 if (null == roi) return;
1389 Area a = getArea(layer_id);
1390 Area asr = M.getArea(roi).createTransformedArea(this.at.createInverse());
1391 if (null == a) {
1392 ht_areas.put(layer_id, asr);
1393 } else {
1394 a.add(asr);
1396 calculateBoundingBox();
1397 updateInDatabase("points=" + layer_id);
1399 /** Subtracts the given ROI, which is expected in world/LayerSet coordinates, to the area present at Layer with id layer_id, or set it if none present yet. */
1400 public void subtract(final long layer_id, final ShapeRoi roi) throws NoninvertibleTransformException {
1401 if (null == roi) return;
1402 Area a = getArea(layer_id);
1403 if (null == a) return;
1404 a.subtract(M.getArea(roi).createTransformedArea(this.at.createInverse()));
1405 calculateBoundingBox();
1406 updateInDatabase("points=" + layer_id);
1409 /** Subtracts the given ROI, and then creates a new AreaList with identical properties and the content of the subtracted part. Returns null if there is no intersection between sroi and the Area for layer_id. */
1410 public AreaList part(final long layer_id, final ShapeRoi sroi) throws NoninvertibleTransformException {
1411 // The Area to subtract, in world coordinates:
1412 Area sub = M.getArea(sroi);
1413 // The area to subtract from:
1414 Area a = getArea(layer_id);
1415 if (null == a || M.isEmpty(a)) return null;
1416 // The intersection:
1417 Area inter = a.createTransformedArea(this.at);
1418 inter.intersect(sub);
1419 if (M.isEmpty(inter)) return null;
1421 // Subtract from this:
1422 this.subtract(layer_id, sroi);
1424 // Create new AreaList with the intersection area, and add it to the same LayerSet as this:
1425 AreaList ali = new AreaList(this.project, this.title, 0, 0);
1426 ali.color = new Color(color.getRed(), color.getGreen(), color.getBlue());
1427 ali.visible = this.visible;
1428 ali.alpha = this.alpha;
1429 ali.addArea(layer_id, inter);
1430 this.layer_set.add(ali); // needed to call updateBucket
1431 ali.calculateBoundingBox();
1433 return ali;
1436 public void keyPressed(KeyEvent ke) {
1437 Object source = ke.getSource();
1438 if (! (source instanceof DisplayCanvas)) return;
1439 DisplayCanvas dc = (DisplayCanvas)source;
1440 Layer la = dc.getDisplay().getLayer();
1441 int keyCode = ke.getKeyCode();
1443 try {
1444 switch (keyCode) {
1445 case KeyEvent.VK_C: // COPY
1446 Area area = (Area) ht_areas.get(la.getId());
1447 if (null != area) {
1448 DisplayCanvas.setCopyBuffer(AreaList.class, area.createTransformedArea(this.at));
1450 ke.consume();
1451 return;
1452 case KeyEvent.VK_V: // PASTE
1453 // Casting a null is fine, and addArea survives a null.
1454 Area a = (Area) DisplayCanvas.getCopyBuffer(AreaList.class);
1455 if (null != a) {
1456 addArea(la.getId(), a.createTransformedArea(this.at.createInverse()));
1457 calculateBoundingBox();
1459 ke.consume();
1460 return;
1461 case KeyEvent.VK_F: // fill all holes
1462 fillHoles(la);
1463 ke.consume();
1464 return;
1465 case KeyEvent.VK_X: // remove area from current layer, if any
1466 if (null != ht_areas.remove(la.getId())) {
1467 calculateBoundingBox();
1469 ke.consume();
1470 return;
1472 } catch (Exception e) {
1473 IJError.print(e);
1474 } finally {
1475 if (ke.isConsumed()) {
1476 Display.repaint(la, getBoundingBox(), 5);
1477 linkPatches();
1478 return;
1482 Roi roi = dc.getFakeImagePlus().getRoi();
1483 if (null == roi) return;
1484 // Check ROI
1485 if (!M.isAreaROI(roi)) {
1486 Utils.log("AreaList only accepts region ROIs, not lines.");
1487 return;
1489 ShapeRoi sroi = new ShapeRoi(roi);
1490 long layer_id = la.getId();
1491 try {
1492 switch (keyCode) {
1493 case KeyEvent.VK_A:
1494 add(layer_id, sroi);
1495 ke.consume();
1496 break;
1497 case KeyEvent.VK_D: // VK_S is for 'save' always
1498 subtract(layer_id, sroi);
1499 ke.consume();
1500 break;
1501 case KeyEvent.VK_K: // knive
1502 AreaList p = part(layer_id, sroi);
1503 if (null != p) {
1504 project.getProjectTree().addSibling(this, p);
1506 ke.consume();
1508 Display.repaint(la, getBoundingBox(), 5);
1509 linkPatches();
1510 } catch (NoninvertibleTransformException e) {
1511 Utils.log("Could not add ROI to area at layer " + dc.getDisplay().getLayer() + " : " + e);
1515 /** Measure the volume (in voxels) of this AreaList,
1516 * and the surface area, the latter estimated as the number of voxels that
1517 * make the outline.
1519 * If the width and height of this AreaList cannot be fit in memory, scaled down versions will be used,
1520 * and thus the results will be approximate.
1522 * */
1523 public String getInfo() {
1524 if (0 == ht_areas.size()) return "Empty AreaList " + this.toString();
1525 Rectangle box = getBoundingBox(null);
1526 float scale = 1.0f;
1527 while (!getProject().getLoader().releaseToFit(2 * (long)(scale * (box.width * box.height)) + 1000000)) { // factor of 2, because a mask will be involved
1528 scale /= 2;
1530 double volume = 0;
1531 double surface = 0;
1532 final int w = (int)Math.ceil(box.width * scale);
1533 final int h = (int)Math.ceil(box.height * scale);
1534 BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1535 Graphics2D g = bi.createGraphics();
1536 //DataBufferByte buffer = (DataBufferByte)bi.getRaster().getDataBuffer();
1537 byte[] pixels = ((DataBufferByte)bi.getRaster().getDataBuffer()).getData(); // buffer.getData();
1539 // prepare suitable transform
1540 AffineTransform aff = (AffineTransform)this.at.clone();
1541 AffineTransform aff2 = new AffineTransform();
1542 // A - remove translation
1543 aff2.translate(-box.x, -box.y);
1544 aff.preConcatenate(aff2);
1545 // B - scale
1546 if (1.0f != scale) {
1547 aff2.setToIdentity();
1548 aff2.translate(box.width/2, box.height/2);
1549 aff2.scale(scale, scale);
1550 aff2.translate(-box.width/2, -box.height/2);
1551 aff.preConcatenate(aff2);
1553 // for each area, measure its area and its perimeter
1554 for (Iterator it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
1555 // fetch Area
1556 Map.Entry entry = (Map.Entry)it.next();
1557 Object ob_area = entry.getValue();
1558 long lid = ((Long)entry.getKey()).longValue();
1559 if (UNLOADED == ob_area) ob_area = loadLayer(lid);
1560 Area area2 = ((Area)ob_area).createTransformedArea(aff);
1561 // paint the area, filling mode
1562 g.setColor(Color.white);
1563 g.fill(area2);
1564 double n_pix = 0;
1565 // count white pixels
1566 for (int i=0; i<pixels.length; i++) {
1567 if (255 == (pixels[i]&0xff)) n_pix++;
1568 // could set the pixel to 0, but I have no idea if that holds properly (or is fast at all) in automatically accelerated images
1570 // debug: show me
1571 // new ImagePlus("lid=" + lid, bi).show();
1574 double thickness = layer_set.getLayer(lid).getThickness();
1575 volume += n_pix * thickness;
1576 // reset board (filling all, to make sure there are no rounding surprises)
1577 g.setColor(Color.black);
1578 g.fillRect(0, 0, w, h);
1579 // now measure length of perimeter
1580 ArrayList al_paths = getPaths(lid);
1581 double length = 0;
1582 for (Iterator ipath = al_paths.iterator(); ipath.hasNext(); ) {
1583 ArrayList path = (ArrayList)ipath.next();
1584 Point p2 = (Point)path.get(0);
1585 for (int i=path.size()-1; i>-1; i--) {
1586 Point p1 = (Point)path.get(i);
1587 length += p1.distance(p2);
1588 p1 = p2;
1591 surface += length * thickness;
1593 // cleanup
1594 pixels = null;
1595 g = null;
1596 bi.flush();
1597 // correct scale
1598 volume /= scale;
1599 surface /= scale;
1600 // remove pretentious after-comma digits on return:
1601 return new StringBuffer("Volume: ").append(IJ.d2s(volume, 2)).append(" (cubic pixels)\nLateral surface: ").append(IJ.d2s(surface, 2)).append(" (square pixels)\n").toString();
1604 /** @param area is expected in world coordinates. */
1605 public boolean intersects(final Area area, final double z_first, final double z_last) {
1606 for (Iterator<Map.Entry> it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
1607 Map.Entry entry = it.next();
1608 Layer layer = layer_set.getLayer(((Long)entry.getKey()).longValue());
1609 if (layer.getZ() >= z_first && layer.getZ() <= z_last) {
1610 Area a = ((Area)entry.getValue()).createTransformedArea(this.at);
1611 a.intersect(area);
1612 Rectangle r = a.getBounds();
1613 if (0 != r.width && 0 != r.height) return true;
1616 return false;
1619 /** Export all given AreaLists as one per pixel value, what is called a "labels" file; a file dialog is offered to save the image as a tiff stack. */
1620 static public void exportAsLabels(final java.util.List<Displayable> list, final ij.gui.Roi roi, final float scale, int first_layer, int last_layer, final boolean visible_only, final boolean to_file, final boolean as_amira_labels) {
1621 // survive everything:
1622 if (null == list || 0 == list.size()) {
1623 Utils.log("Null or empty list.");
1624 return;
1626 if (scale < 0 || scale > 1) {
1627 Utils.log("Improper scale value. Must be 0 < scale <= 1");
1628 return;
1631 // Current AmiraMeshEncoder supports ByteProcessor only: 256 labels max, including background at zero.
1632 if (as_amira_labels && list.size() > 255) {
1633 Utils.log("Saving ONLY first 255 AreaLists!\nDiscarded:");
1634 StringBuffer sb = new StringBuffer();
1635 for (final Displayable d : list.subList(256, list.size())) {
1636 sb.append(" ").append(d.getProject().getShortMeaningfulTitle(d)).append('\n');
1638 Utils.log(sb.toString());
1639 ArrayList<Displayable> li = new ArrayList<Displayable>(list);
1640 list.clear();
1641 list.addAll(li.subList(0, 256));
1644 String path = null;
1645 if (to_file) {
1646 String ext = as_amira_labels ? ".am" : ".tif";
1647 File f = Utils.chooseFile("labels", ext);
1648 if (null == f) return;
1649 path = f.getAbsolutePath().replace('\\','/');
1652 LayerSet layer_set = list.get(0).getLayerSet();
1653 if (first_layer > last_layer) {
1654 int tmp = first_layer;
1655 first_layer = last_layer;
1656 last_layer = tmp;
1657 if (first_layer < 0) first_layer = 0;
1658 if (last_layer >= layer_set.size()) last_layer = layer_set.size()-1;
1660 // Create image according to roi and scale
1661 int width, height;
1662 Rectangle broi = null;
1663 if (null == roi) {
1664 width = (int)(layer_set.getLayerWidth() * scale);
1665 height = (int)(layer_set.getLayerHeight() * scale);
1666 } else {
1667 broi = roi.getBounds();
1668 width = (int)(broi.width * scale);
1669 height = (int)(broi.height * scale);
1672 // Compute highest label value, which affects of course the stack image type
1673 TreeSet<Integer> label_values = new TreeSet<Integer>();
1674 for (final Displayable d : list) {
1675 String label = d.getProperty("label");
1676 if (null != label) label_values.add(Integer.parseInt(label));
1678 int lowest = 0;
1679 int highest = 0;
1680 if (label_values.size() > 0) {
1681 lowest = label_values.first();
1682 highest = label_values.last();
1684 int n_non_labeled = list.size() - label_values.size();
1685 int max_label_value = highest + n_non_labeled;
1687 final ImageStack stack = new ImageStack(width, height);
1688 // processor type:
1689 int type = ImagePlus.GRAY8;
1690 if (max_label_value > 255) { // 0 is background, and 255 different arealists
1691 type = ImagePlus.GRAY16;
1692 if (max_label_value > 65535) { // 0 is background, and 65535 different arealists
1693 type = ImagePlus.GRAY32;
1696 Calibration cal = layer_set.getCalibration();
1698 String amira_params = null;
1699 if (as_amira_labels) {
1700 final StringBuffer sb = new StringBuffer("CoordType \"uniform\"\nMaterials {\nExterior {\n Id 0,\nColor 0 0 0\n}\n");
1701 final float[] c = new float[3];
1702 int value = 0;
1703 for (final Displayable d : list) {
1704 value++; // 0 is background
1705 d.getColor().getRGBColorComponents(c);
1706 String s = d.getProject().getShortMeaningfulTitle(d);
1707 s = s.replace('-', '_').replaceAll(" #", " id");
1708 sb.append(Utils.makeValidIdentifier(s)).append(" {\n")
1709 .append("Id ").append(value).append(",\n")
1710 .append("Color ").append(c[0]).append(' ').append(c[1]).append(' ').append(c[2]).append("\n}\n");
1712 sb.append("}\n");
1713 amira_params = sb.toString();
1716 int count = 1;
1717 final float len = last_layer - first_layer + 1;
1719 // Assign labels
1720 final HashMap<Displayable,Integer> labels = new HashMap<Displayable,Integer>();
1721 for (final Displayable d : list) {
1722 if (visible_only && !d.isVisible()) continue;
1723 String slabel = d.getProperty("label");
1724 int label;
1725 if (null != slabel) {
1726 label = Integer.parseInt(slabel);
1727 } else {
1728 label = (++highest); // 0 is background
1730 labels.put(d, label);
1733 for (Layer la : layer_set.getLayers().subList(first_layer, last_layer+1)) {
1734 Utils.showProgress(count/len);
1735 count++;
1736 ImageProcessor ip = Utils.createProcessor(type, width, height);
1737 if (!(ip instanceof ByteProcessor)) {
1738 ip.setMinAndMax(lowest, highest);
1740 // paint here all arealist that paint to the layer 'la'
1741 for (final Displayable d : list) {
1742 if (visible_only && !d.isVisible()) continue;
1743 ip.setValue(labels.get(d));
1744 AreaList ali = (AreaList)d;
1745 Area area = ali.getArea(la);
1746 if (null == area) {
1747 Utils.log2("Layer " + la + " id: " + d.getId() + " area is " + area);
1748 continue;
1750 // Transform: the scale and the roi
1751 AffineTransform aff = new AffineTransform();
1752 // reverse order of transformations:
1753 /* 3 - To scale: */ if (1 != scale) aff.scale(scale, scale);
1754 /* 2 - To roi coordinates: */ if (null != broi) aff.translate(-broi.x, -broi.y);
1755 /* 1 - To world coordinates: */ aff.concatenate(ali.at);
1756 ShapeRoi sroi = new ShapeRoi(aff.createTransformedShape(area));
1757 ip.setRoi(sroi);
1758 ip.fill(sroi.getMask());
1760 stack.addSlice(la.getZ() * cal.pixelWidth + "", ip);
1762 Utils.showProgress(1);
1763 // Save via file dialog:
1764 ImagePlus imp = new ImagePlus("Labels", stack);
1765 if (as_amira_labels) imp.setProperty("Info", amira_params);
1766 imp.setCalibration(layer_set.getCalibrationCopy());
1767 if (to_file) {
1768 if (as_amira_labels) {
1769 AmiraMeshEncoder ame = new AmiraMeshEncoder(path);
1770 if (!ame.open()) {
1771 Utils.log("Could not write to file " + path);
1772 return;
1774 if (!ame.write(imp)) {
1775 Utils.log("Error in writing Amira file!");
1776 return;
1778 } else {
1779 new FileSaver(imp).saveAsTiff(path);
1781 } else imp.show();
1784 public ResultsTable measure(ResultsTable rt) {
1785 if (0 == ht_areas.size()) return rt;
1786 if (null == rt) rt = Utils.createResultsTable("AreaList results", new String[]{"id", "volume", "name-id"});
1787 rt.incrementCounter();
1788 rt.addLabel("units", "cubic " + layer_set.getCalibration().getUnit());
1789 rt.addValue(0, this.id);
1790 rt.addValue(1, measureVolume());
1791 rt.addValue(2, getNameId());
1792 return rt;
1795 public double measureVolume() {
1796 if (0 == ht_areas.size()) return 0;
1797 Rectangle box = getBoundingBox(null);
1798 float scale = 1.0f;
1799 while (!getProject().getLoader().releaseToFit(2 * (long)(scale * (box.width * box.height)) + 1000000)) { // factor of 2, because a mask will be involved
1800 scale /= 2;
1802 double volume = 0;
1803 double surface = 0;
1804 final int w = (int)Math.ceil(box.width * scale);
1805 final int h = (int)Math.ceil(box.height * scale);
1806 BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1807 Graphics2D g = bi.createGraphics();
1808 //DataBufferByte buffer = (DataBufferByte)bi.getRaster().getDataBuffer();
1809 byte[] pixels = ((DataBufferByte)bi.getRaster().getDataBuffer()).getData(); // buffer.getData();
1811 // prepare suitable transform
1812 AffineTransform aff = (AffineTransform)this.at.clone();
1813 AffineTransform aff2 = new AffineTransform();
1814 // A - remove translation
1815 aff2.translate(-box.x, -box.y);
1816 aff.preConcatenate(aff2);
1817 // B - scale
1818 if (1.0f != scale) {
1819 aff2.setToIdentity();
1820 aff2.translate(box.width/2, box.height/2);
1821 aff2.scale(scale, scale);
1822 aff2.translate(-box.width/2, -box.height/2);
1823 aff.preConcatenate(aff2);
1825 Calibration cal = layer_set.getCalibration();
1826 double pixelWidth = cal.pixelWidth;
1827 double pixelHeight = cal.pixelHeight;
1828 // for each area, measure its area and its perimeter
1829 for (Iterator it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
1830 // fetch Area
1831 Map.Entry entry = (Map.Entry)it.next();
1832 Object ob_area = entry.getValue();
1833 long lid = ((Long)entry.getKey()).longValue();
1834 if (UNLOADED == ob_area) ob_area = loadLayer(lid);
1835 Area area2 = ((Area)ob_area).createTransformedArea(aff);
1836 // paint the area, filling mode
1837 g.setColor(Color.white);
1838 g.fill(area2);
1839 double n_pix = 0;
1840 // count white pixels
1841 for (int i=0; i<pixels.length; i++) {
1842 if (255 == (pixels[i]&0xff)) n_pix++;
1843 // could set the pixel to 0, but I have no idea if that holds properly (or is fast at all) in automatically accelerated images
1845 double thickness = layer_set.getLayer(lid).getThickness();
1846 volume += n_pix * thickness * pixelWidth * pixelHeight * pixelWidth; // the last one is NOT pixelDepth because layer thickness and Z are in pixels
1847 // reset board (filling all, to make sure there are no rounding surprises)
1848 g.setColor(Color.black);
1849 g.fillRect(0, 0, w, h);
1851 // cleanup
1852 pixels = null;
1853 g = null;
1854 bi.flush();
1855 // correct scale
1856 return volume /= (scale * scale); // above, calibration is fixed while computing. Scale only corrects for the 2D plane.
1859 @Override
1860 Class getInternalDataPackageClass() {
1861 return DPAreaList.class;
1864 @Override
1865 Object getDataPackage() {
1866 // The width,height,links,transform and list of areas
1867 return new DPAreaList(this);
1870 static private final class DPAreaList extends Displayable.DataPackage {
1871 final protected HashMap ht;
1872 DPAreaList(final AreaList ali) {
1873 super(ali);
1874 this.ht = new HashMap();
1875 for (final Object entry : ali.ht_areas.entrySet()) {
1876 Map.Entry e = (Map.Entry)entry;
1877 Object area = e.getValue();
1878 if (area.getClass() == Area.class) area = new Area((Area)area);
1879 this.ht.put(e.getKey(), area);
1882 final boolean to2(final Displayable d) {
1883 super.to1(d);
1884 final AreaList ali = (AreaList)d;
1885 ali.ht_areas.clear();
1886 for (final Object entry : ht.entrySet()) {
1887 final Map.Entry e = (Map.Entry)entry;
1888 Object area = e.getValue();
1889 if (area.getClass() == Area.class) area = new Area((Area)area);
1890 ali.ht_areas.put(e.getKey(), area);
1892 return true;