Fixes to homogenizeContrast
[trakem2.git] / ini / trakem2 / display / AreaList.java
blobdd7bcdacc881ccfd4cbca7ffe269be0dabc86200
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006, 2007 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(double scale, int resample) {
1144 // in the LayerSet, layers are ordered by Z already.
1145 try {
1146 if (resample <=0 ) {
1147 resample = 1;
1148 Utils.log2("Fixing zero or negative resampling value to 1.");
1150 int n = getNAreas();
1151 if (0 == n) return null;
1152 final Rectangle r = getBoundingBox();
1153 // remove translation from a copy of this Displayable's AffineTransform
1154 final AffineTransform at_translate = new AffineTransform();
1155 at_translate.translate(-r.x, -r.y);
1156 final AffineTransform at2 = (AffineTransform)this.at.clone();
1157 at2.preConcatenate(at_translate);
1158 // incorporate resampling scaling into the transform
1159 final AffineTransform atK = new AffineTransform();
1160 //Utils.log("resample: " + resample + " scale: " + scale);
1161 final double K = (1.0 / resample) * scale; // 'scale' is there to limit gigantic universes
1162 final Calibration cal = layer_set.getCalibrationCopy();
1163 atK.scale(K, K);
1164 at2.preConcatenate(atK);
1166 ImageStack stack = null;
1167 float z_first = 0;
1168 double thickness = 1;
1169 final int w = (int)Math.ceil(r.width * K);
1170 final int h = (int)Math.ceil(r.height * K);
1172 final TreeMap<Double,Layer> assigned = new TreeMap<Double,Layer>();
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 for (final Layer la : layer_set.getLayers()) {
1182 if (0 == n) break; // no more areas to paint
1183 final Area area = getArea(la);
1184 if (null != area) {
1185 if (null == stack) {
1186 //Utils.log2("0 - creating stack with w,h : " + w + ", " + h);
1187 stack = new ImageStack(w, h);
1188 z_first = (float)la.getZ(); // z of the first layer
1189 thickness = la.getThickness();
1190 assigned.put((double)z_first, la);
1191 } else {
1192 assigned.put(z_first + thickness * stack.getSize(), la);
1193 // the layer is added to the stack below
1195 project.getLoader().releaseToFit(w, h, ImagePlus.GRAY8, 3);
1196 // must be a new image, for pixel array is shared with BufferedImage and ByteProcessor in java 1.6
1197 final BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1198 final Graphics2D g = bi.createGraphics();
1199 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
1200 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1201 g.setColor(Color.white);
1202 g.fill(area.createTransformedArea(at2));
1203 final ByteProcessor bp = new ByteProcessor(bi);
1204 bp.threshold(threshold);
1205 stack.addSlice(Double.toString(la.getZ()), bp);
1206 bi.flush();
1208 n--;
1210 } else if (null != stack) {
1211 // add a black slice
1212 stack.addSlice(la.getZ() + "", new ByteProcessor(w, h));
1216 // zero-pad stack
1217 // No need anymore: MCTriangulator does it on its own now
1218 stack = zeroPad(stack);
1220 // Still, the MCTriangulator does NOT zero pad properly
1222 ImagePlus imp = new ImagePlus("", stack);
1223 imp.getCalibration().pixelWidth = cal.pixelWidth * scale;
1224 imp.getCalibration().pixelHeight = cal.pixelHeight * scale;
1225 imp.getCalibration().pixelDepth = thickness * scale; // no need to factor in resampling
1226 //debug:
1227 //imp.show();
1228 //Utils.log2("Stack dimensions: " + imp.getWidth() + ", " + imp.getHeight() + ", " + imp.getStack().getSize());
1229 // end of generating byte[] arrays
1230 // Now marching cubes
1231 final Triangulator tri = new MCTriangulator();
1232 final List list = tri.getTriangles(imp, 0, new boolean[]{true, true, true}, 1);
1233 // now translate all coordinates by x,y,z (it would be nice to simply assign them to a mesh object)
1234 final float dx = (float)(r.x * scale * cal.pixelWidth);
1235 final float dy = (float)(r.y * scale * cal.pixelHeight);
1236 final float dz = (float)((z_first - thickness) * scale * cal.pixelWidth); // the z of the first layer found, corrected for both scale and the zero padding
1237 final float rs = resample / (float)scale;
1238 final float z_correction = (float)(thickness * scale * cal.pixelWidth);
1239 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1240 final Point3f p = (Point3f)it.next();
1241 // fix back the resampling (but not the universe scale, which has already been considered)
1242 p.x *= rs; //resample / scale; // a resampling of '2' means 0.5 (I love inverted worlds..)
1243 p.y *= rs; //resample / scale;
1244 p.z *= cal.pixelWidth;
1245 //Z was not resampled
1246 // translate to the x,y,z coordinate of the object in space
1247 p.x += dx - rs; // minus rs, as an offset for zero-padding
1248 p.y += dy - rs;
1249 p.z += dz + z_correction; // translate one complete section up. I don't fully understand why I need this, but this is correct.
1252 // TODO: should capture vertices whose Z coordinate falls within a layer thickness, and translate that to the real layer Z and thickness (because now it's using the first layer thickness only).
1253 // TODO: even before this, should enable interpolation when desired, since we have images anyway.
1254 // TODO: and even better, when there is only one island per section, give the option to enable VectorStrin2D mesh creation like profiles.
1257 // Check if any layer has an improper Z assigned. The assigned gives a list of entries sorted by Z
1258 double last_real_z = 0;
1259 HashMap<Integer,ArrayList<Point3f>> map = null;
1261 for (final Map.Entry<Double,Layer> e : assigned.entrySet()) {
1262 final double fake_z = e.getKey();
1263 final double real_z = e.getValue().getZ();
1264 final double fake_thickness = thickness;
1265 final double real_thickness = e.getValue().getThickness();
1267 if ( ! M.equals(fake_z, real_z) ) {
1268 if (null == map) {
1269 map = new HashMap<Integer,ArrayList<Point3f>>();
1270 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1271 final Point3f p = (Point3f) it.next();
1272 int lz = (int)( 0.0005 + (p.z - z_first) / thickness );
1273 ArrayList<Point3f> az = map.get(lz);
1274 if (null == az) {
1275 az = new ArrayList<Point3f>();
1276 map.put(lz, az);
1278 az.add(p);
1281 for (final Integer lz : new java.util.TreeSet<Integer>(map.keySet())) {
1282 Utils.log2("Key: " + lz);
1285 // must fix: and also accumulate the difference in the offset, for subsequent calls.
1286 // find all coords between fake_z and fake_z + fake_thickness, and stretch them proportionally to real_z and real_z + real_thickness
1287 int lz = (int)( 0.0005 + (fake_z - z_first) / thickness );
1288 ArrayList<Point3f> az = map.get(lz);
1289 if (null == az) {
1290 Utils.log2("Something is WRONG: null az for lz = " + lz);
1291 continue;
1293 for (final Point3f p : az) {
1294 p.z = (float) (real_z + real_thickness * ( (p.z - fake_z - z_first) / thickness ));
1298 for (final Iterator it = list.iterator(); it.hasNext(); ) {
1299 final Point3f p = (Point3f) it.next();
1300 if (p.z >= fake_z && p.z <= fake_z + thickness) {
1301 // bring to real z + the proportion of the real thickness
1302 p.z = offset + real_z + real_thickness * ( (p.z - fake_z - z_first) / thickness );
1309 return list;
1311 } catch (Exception e) {
1312 e.printStackTrace();
1314 return null;
1317 static private ImageStack zeroPad(final ImageStack stack) {
1318 int w = stack.getWidth();
1319 int h = stack.getHeight();
1320 // enlarge all processors
1321 ImageStack st = new ImageStack(w+2, h+2);
1322 for (int i=1; i<=stack.getSize(); i++) {
1323 ImageProcessor ip = new ByteProcessor(w+2, h+2);
1324 ip.insert(stack.getProcessor(i), 1, 1);
1325 st.addSlice(Integer.toString(i), ip);
1327 ByteProcessor bp = new ByteProcessor(w+2, h+2);
1328 // insert slice at 0
1329 st.addSlice("0", bp, 0);
1330 // append slice at the end
1331 st.addSlice(Integer.toString(stack.getSize()+1), bp);
1333 return st;
1336 /** 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. */
1337 public void setArea(final long layer_id, final Area area) {
1338 if (null == area) return;
1339 ht_areas.put(layer_id, area);
1340 updateInDatabase("points=" + layer_id);
1343 /** 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. */
1344 public void addArea(final long layer_id, final Area area) {
1345 if (null == area) return;
1346 Area a = getArea(layer_id);
1347 if (null == a) ht_areas.put(layer_id, new Area(area));
1348 else a.add(area);
1349 updateInDatabase("points=" + layer_id);
1352 /** 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. */
1353 public void add(final long layer_id, final ShapeRoi roi) throws NoninvertibleTransformException{
1354 if (null == roi) return;
1355 Area a = getArea(layer_id);
1356 Area asr = M.getArea(roi).createTransformedArea(this.at.createInverse());
1357 if (null == a) {
1358 ht_areas.put(layer_id, asr);
1359 } else {
1360 a.add(asr);
1362 calculateBoundingBox();
1363 updateInDatabase("points=" + layer_id);
1365 /** 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. */
1366 public void subtract(final long layer_id, final ShapeRoi roi) throws NoninvertibleTransformException {
1367 if (null == roi) return;
1368 Area a = getArea(layer_id);
1369 if (null == a) return;
1370 a.subtract(M.getArea(roi).createTransformedArea(this.at.createInverse()));
1371 calculateBoundingBox();
1372 updateInDatabase("points=" + layer_id);
1375 /** 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. */
1376 public AreaList part(final long layer_id, final ShapeRoi sroi) throws NoninvertibleTransformException {
1377 // The Area to subtract, in world coordinates:
1378 Area sub = M.getArea(sroi);
1379 // The area to subtract from:
1380 Area a = getArea(layer_id);
1381 if (null == a || M.isEmpty(a)) return null;
1382 // The intersection:
1383 Area inter = a.createTransformedArea(this.at);
1384 inter.intersect(sub);
1385 if (M.isEmpty(inter)) return null;
1387 // Subtract from this:
1388 this.subtract(layer_id, sroi);
1390 // Create new AreaList with the intersection area, and add it to the same LayerSet as this:
1391 AreaList ali = new AreaList(this.project, this.title, 0, 0);
1392 ali.color = new Color(color.getRed(), color.getGreen(), color.getBlue());
1393 ali.visible = this.visible;
1394 ali.alpha = this.alpha;
1395 ali.addArea(layer_id, inter);
1396 this.layer_set.add(ali); // needed to call updateBucket
1397 ali.calculateBoundingBox();
1399 return ali;
1402 public void keyPressed(KeyEvent ke) {
1403 Object source = ke.getSource();
1404 if (! (source instanceof DisplayCanvas)) return;
1405 DisplayCanvas dc = (DisplayCanvas)source;
1406 Layer la = dc.getDisplay().getLayer();
1407 int keyCode = ke.getKeyCode();
1409 try {
1410 switch (keyCode) {
1411 case KeyEvent.VK_C: // COPY
1412 Area area = (Area) ht_areas.get(la.getId());
1413 if (null != area) {
1414 DisplayCanvas.setCopyBuffer(AreaList.class, area.createTransformedArea(this.at));
1416 ke.consume();
1417 return;
1418 case KeyEvent.VK_V: // PASTE
1419 // Casting a null is fine, and addArea survives a null.
1420 Area a = (Area) DisplayCanvas.getCopyBuffer(AreaList.class);
1421 if (null != a) {
1422 addArea(la.getId(), a.createTransformedArea(this.at.createInverse()));
1423 calculateBoundingBox();
1425 ke.consume();
1426 return;
1427 case KeyEvent.VK_F: // fill all holes
1428 fillHoles(la);
1429 ke.consume();
1430 return;
1431 case KeyEvent.VK_X: // remove area from current layer, if any
1432 if (null != ht_areas.remove(la.getId())) {
1433 calculateBoundingBox();
1435 ke.consume();
1436 return;
1438 } catch (Exception e) {
1439 IJError.print(e);
1440 } finally {
1441 if (ke.isConsumed()) {
1442 Display.repaint(la, getBoundingBox(), 5);
1443 linkPatches();
1444 return;
1448 Roi roi = dc.getFakeImagePlus().getRoi();
1449 if (null == roi) return;
1450 // Check ROI
1451 if (!M.isAreaROI(roi)) {
1452 Utils.log("AreaList only accepts region ROIs, not lines.");
1453 return;
1455 ShapeRoi sroi = new ShapeRoi(roi);
1456 long layer_id = la.getId();
1457 try {
1458 switch (keyCode) {
1459 case KeyEvent.VK_A:
1460 add(layer_id, sroi);
1461 ke.consume();
1462 break;
1463 case KeyEvent.VK_D: // VK_S is for 'save' always
1464 subtract(layer_id, sroi);
1465 ke.consume();
1466 break;
1467 case KeyEvent.VK_K: // knive
1468 AreaList p = part(layer_id, sroi);
1469 if (null != p) {
1470 project.getProjectTree().addSibling(this, p);
1472 ke.consume();
1474 Display.repaint(la, getBoundingBox(), 5);
1475 linkPatches();
1476 } catch (NoninvertibleTransformException e) {
1477 Utils.log("Could not add ROI to area at layer " + dc.getDisplay().getLayer() + " : " + e);
1481 /** Measure the volume (in voxels) of this AreaList,
1482 * and the surface area, the latter estimated as the number of voxels that
1483 * make the outline.
1485 * If the width and height of this AreaList cannot be fit in memory, scaled down versions will be used,
1486 * and thus the results will be approximate.
1488 * */
1489 public String getInfo() {
1490 if (0 == ht_areas.size()) return "Empty AreaList " + this.toString();
1491 Rectangle box = getBoundingBox(null);
1492 float scale = 1.0f;
1493 while (!getProject().getLoader().releaseToFit(2 * (long)(scale * (box.width * box.height)) + 1000000)) { // factor of 2, because a mask will be involved
1494 scale /= 2;
1496 double volume = 0;
1497 double surface = 0;
1498 final int w = (int)Math.ceil(box.width * scale);
1499 final int h = (int)Math.ceil(box.height * scale);
1500 BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1501 Graphics2D g = bi.createGraphics();
1502 //DataBufferByte buffer = (DataBufferByte)bi.getRaster().getDataBuffer();
1503 byte[] pixels = ((DataBufferByte)bi.getRaster().getDataBuffer()).getData(); // buffer.getData();
1505 // prepare suitable transform
1506 AffineTransform aff = (AffineTransform)this.at.clone();
1507 AffineTransform aff2 = new AffineTransform();
1508 // A - remove translation
1509 aff2.translate(-box.x, -box.y);
1510 aff.preConcatenate(aff2);
1511 // B - scale
1512 if (1.0f != scale) {
1513 aff2.setToIdentity();
1514 aff2.translate(box.width/2, box.height/2);
1515 aff2.scale(scale, scale);
1516 aff2.translate(-box.width/2, -box.height/2);
1517 aff.preConcatenate(aff2);
1519 // for each area, measure its area and its perimeter
1520 for (Iterator it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
1521 // fetch Area
1522 Map.Entry entry = (Map.Entry)it.next();
1523 Object ob_area = entry.getValue();
1524 long lid = ((Long)entry.getKey()).longValue();
1525 if (UNLOADED == ob_area) ob_area = loadLayer(lid);
1526 Area area2 = ((Area)ob_area).createTransformedArea(aff);
1527 // paint the area, filling mode
1528 g.setColor(Color.white);
1529 g.fill(area2);
1530 double n_pix = 0;
1531 // count white pixels
1532 for (int i=0; i<pixels.length; i++) {
1533 if (255 == (pixels[i]&0xff)) n_pix++;
1534 // could set the pixel to 0, but I have no idea if that holds properly (or is fast at all) in automatically accelerated images
1536 // debug: show me
1537 // new ImagePlus("lid=" + lid, bi).show();
1540 double thickness = layer_set.getLayer(lid).getThickness();
1541 volume += n_pix * thickness;
1542 // reset board (filling all, to make sure there are no rounding surprises)
1543 g.setColor(Color.black);
1544 g.fillRect(0, 0, w, h);
1545 // now measure length of perimeter
1546 ArrayList al_paths = getPaths(lid);
1547 double length = 0;
1548 for (Iterator ipath = al_paths.iterator(); ipath.hasNext(); ) {
1549 ArrayList path = (ArrayList)ipath.next();
1550 Point p2 = (Point)path.get(0);
1551 for (int i=path.size()-1; i>-1; i--) {
1552 Point p1 = (Point)path.get(i);
1553 length += p1.distance(p2);
1554 p1 = p2;
1557 surface += length * thickness;
1559 // cleanup
1560 pixels = null;
1561 g = null;
1562 bi.flush();
1563 // correct scale
1564 volume /= scale;
1565 surface /= scale;
1566 // remove pretentious after-comma digits on return:
1567 return new StringBuffer("Volume: ").append(IJ.d2s(volume, 2)).append(" (cubic pixels)\nLateral surface: ").append(IJ.d2s(surface, 2)).append(" (square pixels)\n").toString();
1570 /** @param area is expected in world coordinates. */
1571 public boolean intersects(final Area area, final double z_first, final double z_last) {
1572 for (Iterator<Map.Entry> it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
1573 Map.Entry entry = it.next();
1574 Layer layer = layer_set.getLayer(((Long)entry.getKey()).longValue());
1575 if (layer.getZ() >= z_first && layer.getZ() <= z_last) {
1576 Area a = ((Area)entry.getValue()).createTransformedArea(this.at);
1577 a.intersect(area);
1578 Rectangle r = a.getBounds();
1579 if (0 != r.width && 0 != r.height) return true;
1582 return false;
1585 /** 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. */
1586 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) {
1587 // survive everything:
1588 if (null == list || 0 == list.size()) {
1589 Utils.log("Null or empty list.");
1590 return;
1592 if (scale < 0 || scale > 1) {
1593 Utils.log("Improper scale value. Must be 0 < scale <= 1");
1594 return;
1597 // Current AmiraMeshEncoder supports ByteProcessor only: 256 labels max, including background at zero.
1598 if (as_amira_labels && list.size() > 255) {
1599 Utils.log("Saving ONLY first 255 AreaLists!\nDiscarded:");
1600 StringBuffer sb = new StringBuffer();
1601 for (final Displayable d : list.subList(256, list.size())) {
1602 sb.append(" ").append(d.getProject().getShortMeaningfulTitle(d)).append('\n');
1604 Utils.log(sb.toString());
1605 ArrayList<Displayable> li = new ArrayList<Displayable>(list);
1606 list.clear();
1607 list.addAll(li.subList(0, 256));
1610 String path = null;
1611 if (to_file) {
1612 String ext = as_amira_labels ? ".am" : ".tif";
1613 File f = Utils.chooseFile("labels", ext);
1614 if (null == f) return;
1615 path = f.getAbsolutePath().replace('\\','/');
1618 LayerSet layer_set = list.get(0).getLayerSet();
1619 if (first_layer > last_layer) {
1620 int tmp = first_layer;
1621 first_layer = last_layer;
1622 last_layer = tmp;
1623 if (first_layer < 0) first_layer = 0;
1624 if (last_layer >= layer_set.size()) last_layer = layer_set.size()-1;
1626 // Create image according to roi and scale
1627 int width, height;
1628 Rectangle broi = null;
1629 if (null == roi) {
1630 width = (int)(layer_set.getLayerWidth() * scale);
1631 height = (int)(layer_set.getLayerHeight() * scale);
1632 } else {
1633 broi = roi.getBounds();
1634 width = (int)(broi.width * scale);
1635 height = (int)(broi.height * scale);
1638 // Compute highest label value, which affects of course the stack image type
1639 TreeSet<Integer> label_values = new TreeSet<Integer>();
1640 for (final Displayable d : list) {
1641 String label = d.getProperty("label");
1642 if (null != label) label_values.add(Integer.parseInt(label));
1644 int lowest = 0;
1645 int highest = 0;
1646 if (label_values.size() > 0) {
1647 lowest = label_values.first();
1648 highest = label_values.last();
1650 int n_non_labeled = list.size() - label_values.size();
1651 int max_label_value = highest + n_non_labeled;
1653 final ImageStack stack = new ImageStack(width, height);
1654 // processor type:
1655 int type = ImagePlus.GRAY8;
1656 if (max_label_value > 255) { // 0 is background, and 255 different arealists
1657 type = ImagePlus.GRAY16;
1658 if (max_label_value > 65535) { // 0 is background, and 65535 different arealists
1659 type = ImagePlus.GRAY32;
1662 Calibration cal = layer_set.getCalibration();
1664 String amira_params = null;
1665 if (as_amira_labels) {
1666 final StringBuffer sb = new StringBuffer("CoordType \"uniform\"\nMaterials {\nExterior {\n Id 0,\nColor 0 0 0\n}\n");
1667 final float[] c = new float[3];
1668 int value = 0;
1669 for (final Displayable d : list) {
1670 value++; // 0 is background
1671 d.getColor().getRGBColorComponents(c);
1672 String s = d.getProject().getShortMeaningfulTitle(d);
1673 s = s.replace('-', '_').replaceAll(" #", " id");
1674 sb.append(Utils.makeValidIdentifier(s)).append(" {\n")
1675 .append("Id ").append(value).append(",\n")
1676 .append("Color ").append(c[0]).append(' ').append(c[1]).append(' ').append(c[2]).append("\n}\n");
1678 sb.append("}\n");
1679 amira_params = sb.toString();
1682 int count = 1;
1683 final float len = last_layer - first_layer + 1;
1685 // Assign labels
1686 final HashMap<Displayable,Integer> labels = new HashMap<Displayable,Integer>();
1687 for (final Displayable d : list) {
1688 if (visible_only && !d.isVisible()) continue;
1689 String slabel = d.getProperty("label");
1690 int label;
1691 if (null != slabel) {
1692 label = Integer.parseInt(slabel);
1693 } else {
1694 label = (++highest); // 0 is background
1696 labels.put(d, label);
1699 for (Layer la : layer_set.getLayers().subList(first_layer, last_layer+1)) {
1700 Utils.showProgress(count/len);
1701 count++;
1702 ImageProcessor ip = Utils.createProcessor(type, width, height);
1703 if (!(ip instanceof ByteProcessor)) {
1704 ip.setMinAndMax(lowest, highest);
1706 // paint here all arealist that paint to the layer 'la'
1707 for (final Displayable d : list) {
1708 if (visible_only && !d.isVisible()) continue;
1709 ip.setValue(labels.get(d));
1710 AreaList ali = (AreaList)d;
1711 Area area = ali.getArea(la);
1712 if (null == area) {
1713 Utils.log2("Layer " + la + " id: " + d.getId() + " area is " + area);
1714 continue;
1716 // Transform: the scale and the roi
1717 AffineTransform aff = new AffineTransform();
1718 // reverse order of transformations:
1719 /* 3 - To scale: */ if (1 != scale) aff.scale(scale, scale);
1720 /* 2 - To roi coordinates: */ if (null != broi) aff.translate(-broi.x, -broi.y);
1721 /* 1 - To world coordinates: */ aff.concatenate(ali.at);
1722 ShapeRoi sroi = new ShapeRoi(aff.createTransformedShape(area));
1723 ip.setRoi(sroi);
1724 ip.fill(sroi.getMask());
1726 stack.addSlice(la.getZ() * cal.pixelWidth + "", ip);
1728 Utils.showProgress(1);
1729 // Save via file dialog:
1730 ImagePlus imp = new ImagePlus("Labels", stack);
1731 if (as_amira_labels) imp.setProperty("Info", amira_params);
1732 imp.setCalibration(layer_set.getCalibrationCopy());
1733 if (to_file) {
1734 if (as_amira_labels) {
1735 AmiraMeshEncoder ame = new AmiraMeshEncoder(path);
1736 if (!ame.open()) {
1737 Utils.log("Could not write to file " + path);
1738 return;
1740 if (!ame.write(imp)) {
1741 Utils.log("Error in writing Amira file!");
1742 return;
1744 } else {
1745 new FileSaver(imp).saveAsTiff(path);
1747 } else imp.show();
1750 public ResultsTable measure(ResultsTable rt) {
1751 if (0 == ht_areas.size()) return rt;
1752 if (null == rt) rt = Utils.createResultsTable("AreaList results", new String[]{"id", "volume", "name-id"});
1753 rt.incrementCounter();
1754 rt.addLabel("units", "cubic " + layer_set.getCalibration().getUnit());
1755 rt.addValue(0, this.id);
1756 rt.addValue(1, measureVolume());
1757 rt.addValue(2, getNameId());
1758 return rt;
1761 public double measureVolume() {
1762 if (0 == ht_areas.size()) return 0;
1763 Rectangle box = getBoundingBox(null);
1764 float scale = 1.0f;
1765 while (!getProject().getLoader().releaseToFit(2 * (long)(scale * (box.width * box.height)) + 1000000)) { // factor of 2, because a mask will be involved
1766 scale /= 2;
1768 double volume = 0;
1769 double surface = 0;
1770 final int w = (int)Math.ceil(box.width * scale);
1771 final int h = (int)Math.ceil(box.height * scale);
1772 BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1773 Graphics2D g = bi.createGraphics();
1774 //DataBufferByte buffer = (DataBufferByte)bi.getRaster().getDataBuffer();
1775 byte[] pixels = ((DataBufferByte)bi.getRaster().getDataBuffer()).getData(); // buffer.getData();
1777 // prepare suitable transform
1778 AffineTransform aff = (AffineTransform)this.at.clone();
1779 AffineTransform aff2 = new AffineTransform();
1780 // A - remove translation
1781 aff2.translate(-box.x, -box.y);
1782 aff.preConcatenate(aff2);
1783 // B - scale
1784 if (1.0f != scale) {
1785 aff2.setToIdentity();
1786 aff2.translate(box.width/2, box.height/2);
1787 aff2.scale(scale, scale);
1788 aff2.translate(-box.width/2, -box.height/2);
1789 aff.preConcatenate(aff2);
1791 Calibration cal = layer_set.getCalibration();
1792 double pixelWidth = cal.pixelWidth;
1793 double pixelHeight = cal.pixelHeight;
1794 // for each area, measure its area and its perimeter
1795 for (Iterator it = ht_areas.entrySet().iterator(); it.hasNext(); ) {
1796 // fetch Area
1797 Map.Entry entry = (Map.Entry)it.next();
1798 Object ob_area = entry.getValue();
1799 long lid = ((Long)entry.getKey()).longValue();
1800 if (UNLOADED == ob_area) ob_area = loadLayer(lid);
1801 Area area2 = ((Area)ob_area).createTransformedArea(aff);
1802 // paint the area, filling mode
1803 g.setColor(Color.white);
1804 g.fill(area2);
1805 double n_pix = 0;
1806 // count white pixels
1807 for (int i=0; i<pixels.length; i++) {
1808 if (255 == (pixels[i]&0xff)) n_pix++;
1809 // could set the pixel to 0, but I have no idea if that holds properly (or is fast at all) in automatically accelerated images
1811 double thickness = layer_set.getLayer(lid).getThickness();
1812 volume += n_pix * thickness * pixelWidth * pixelHeight * pixelWidth; // the last one is NOT pixelDepth because layer thickness and Z are in pixels
1813 // reset board (filling all, to make sure there are no rounding surprises)
1814 g.setColor(Color.black);
1815 g.fillRect(0, 0, w, h);
1817 // cleanup
1818 pixels = null;
1819 g = null;
1820 bi.flush();
1821 // correct scale
1822 return volume /= (scale * scale); // above, calibration is fixed while computing. Scale only corrects for the 2D plane.
1825 @Override
1826 Class getInternalDataPackageClass() {
1827 return DPAreaList.class;
1830 @Override
1831 Object getDataPackage() {
1832 // The width,height,links,transform and list of areas
1833 return new DPAreaList(this);
1836 static private final class DPAreaList extends Displayable.DataPackage {
1837 final protected HashMap ht;
1838 DPAreaList(final AreaList ali) {
1839 super(ali);
1840 this.ht = new HashMap();
1841 for (final Object entry : ali.ht_areas.entrySet()) {
1842 Map.Entry e = (Map.Entry)entry;
1843 Object area = e.getValue();
1844 if (area.getClass() == Area.class) area = new Area((Area)area);
1845 this.ht.put(e.getKey(), area);
1848 final boolean to2(final Displayable d) {
1849 super.to1(d);
1850 final AreaList ali = (AreaList)d;
1851 ali.ht_areas.clear();
1852 for (final Object entry : ht.entrySet()) {
1853 final Map.Entry e = (Map.Entry)entry;
1854 Object area = e.getValue();
1855 if (area.getClass() == Area.class) area = new Area((Area)area);
1856 ali.ht_areas.put(e.getKey(), area);
1858 return true;