Added automatic GUI to fix file paths.
[trakem2.git] / ini / trakem2 / display / Patch.java
blobf14a2bf8282ccd9dbb52ac7612475efeb4f1629f
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.display;
26 import ij.ImagePlus;
27 import ij.gui.GenericDialog;
28 import ij.gui.Roi;
29 import ij.gui.ShapeRoi;
30 import ij.gui.Toolbar;
31 import ij.process.ByteProcessor;
32 import ij.process.ImageProcessor;
33 import ini.trakem2.Project;
34 import ini.trakem2.imaging.PatchStack;
35 import ini.trakem2.utils.Utils;
36 import ini.trakem2.utils.IJError;
37 import ini.trakem2.utils.Search;
38 import ini.trakem2.utils.Worker;
39 import ini.trakem2.utils.Bureaucrat;
40 import ini.trakem2.persistence.Loader;
41 import ini.trakem2.vector.VectorString3D;
43 import java.awt.Dimension;
44 import java.awt.Rectangle;
45 import java.awt.Event;
46 import java.awt.Image;
47 import java.awt.Color;
48 import java.awt.Composite;
49 import java.awt.AlphaComposite;
50 import java.awt.Toolkit;
51 import java.awt.Graphics2D;
52 import java.awt.image.BufferedImage;
53 import java.awt.image.MemoryImageSource;
54 import java.awt.image.DirectColorModel;
55 import java.awt.geom.AffineTransform;
56 import java.awt.geom.Area;
57 import java.awt.geom.Point2D;
58 import java.awt.Polygon;
59 import java.awt.geom.PathIterator;
60 import java.awt.geom.NoninvertibleTransformException;
61 import java.awt.image.PixelGrabber;
62 import java.awt.event.KeyEvent;
63 import java.util.Iterator;
64 import java.util.Map;
65 import java.util.HashMap;
66 import java.util.ArrayList;
67 import java.util.HashSet;
68 import java.util.Collection;
69 import java.io.File;
71 import mpicbg.models.AffineModel2D;
72 import mpicbg.trakem2.transform.CoordinateTransform;
73 import mpicbg.trakem2.transform.TransformMesh;
74 import mpicbg.trakem2.transform.CoordinateTransformList;
75 import mpicbg.trakem2.transform.TransformMeshMapping;
77 public final class Patch extends Displayable {
79 private int type = -1; // unknown
80 /** The channels that the currently existing awt image has ready for painting. */
81 private int channels = 0xffffffff;
83 /** To generate contrasted images non-destructively. */
84 private double min = 0;
85 private double max = 255;
87 private int o_width = 0, o_height = 0;
89 /** To be set after the first successful query on whether this file exists, from the Loader, via the setCurrentPath method. This works as a good path cache to avoid excessive calls to File.exists(), which shows up as a huge performance drag. */
90 private String current_path = null;
91 /** To be read from XML, or set when the file ImagePlus has been updated and the current_path points to something else. */
92 private String original_path = null;
94 /** The CoordinateTransform that transfers image data to mipmap image data. The AffineTransform is then applied to the mipmap image data. */
95 private CoordinateTransform ct = null;
97 /** Construct a Patch from an image. */
98 public Patch(Project project, String title, double x, double y, ImagePlus imp) {
99 super(project, title, x, y);
100 this.type = imp.getType();
101 this.min = imp.getProcessor().getMin();
102 this.max = imp.getProcessor().getMax();
103 checkMinMax();
104 this.o_width = imp.getWidth();
105 this.o_height = imp.getHeight();
106 this.width = (int)o_width;
107 this.height = (int)o_height;
108 project.getLoader().cache(this, imp);
109 addToDatabase();
112 /** Reconstruct a Patch from the database. The ImagePlus will be loaded when necessary. */
113 public Patch(Project project, long id, String title, double width, double height, int type, boolean locked, double min, double max, AffineTransform at) {
114 super(project, id, title, locked, at, width, height);
115 this.type = type;
116 this.min = min;
117 this.max = max;
118 if (0 == o_width) o_width = (int)width;
119 if (0 == o_height) o_height = (int)height;
120 checkMinMax();
123 /** Reconstruct from an XML entry. */
124 public Patch(Project project, long id, HashMap ht_attributes, HashMap ht_links) {
125 super(project, id, ht_attributes, ht_links);
126 // cache path:
127 project.getLoader().addedPatchFrom((String)ht_attributes.get("file_path"), this);
128 boolean hasmin = false;
129 boolean hasmax = false;
130 // parse specific fields
131 final Iterator it = ht_attributes.entrySet().iterator();
132 while (it.hasNext()) {
133 final Map.Entry entry = (Map.Entry)it.next();
134 final String key = (String)entry.getKey();
135 final String data = (String)entry.getValue();
136 if (key.equals("type")) {
137 this.type = Integer.parseInt(data);
138 } else if (key.equals("min")) {
139 this.min = Double.parseDouble(data);
140 hasmin = true;
141 } else if (key.equals("max")) {
142 this.max = Double.parseDouble(data);
143 hasmax = true;
144 } else if (key.equals("original_path")) {
145 this.original_path = data;
146 } else if (key.equals("o_width")) {
147 this.o_width = Integer.parseInt(data);
148 } else if (key.equals("o_height")) {
149 this.o_height = Integer.parseInt(data);
153 if (0 == o_width || 0 == o_height) {
154 // The original image width and height are unknown.
155 try {
156 Utils.log2("Restoring original width/height from file for id=" + id);
157 // Use BioFormats to read the dimensions out of the original file's header
158 final Dimension dim = project.getLoader().getDimensions(this);
159 o_width = dim.width;
160 o_height = dim.height;
161 } catch (Exception e) {
162 Utils.log("Could not read source data width/height for patch " + this +"\n --> To fix it, close the project and add o_width=\"XXX\" o_height=\"YYY\"\n to patch entry with oid=\"" + id + "\",\n where o_width,o_height are the image dimensions as defined in the image file.");
163 // So set them to whatever is somewhat survivable for the moment
164 o_width = (int)width;
165 o_height = (int)height;
166 IJError.print(e);
170 if (hasmin && hasmax) {
171 checkMinMax();
172 } else {
173 // standard, from the image, to be defined when first painted
174 min = max = -1;
176 //Utils.log2("new Patch from XML, min and max: " + min + "," + max);
179 /** The original width of the pixels in the source image file. */
180 public int getOWidth() { return o_width; }
181 /** The original height of the pixels in the source image file. */
182 public int getOHeight() { return o_height; }
184 /** Fetches the ImagePlus from the cache; <b>be warned</b>: the returned ImagePlus may have been flushed, removed and then recreated if the program had memory needs that required flushing part of the cache; use @getImageProcessor to get the pixels guaranteed not to be ever null. */
185 public ImagePlus getImagePlus() {
186 final ImagePlus imp = this.project.getLoader().fetchImagePlus(this);
187 return imp;
190 /** Fetches the ImageProcessor from the cache, which will never be flushed or its pixels set to null. If you keep many of these, you may end running out of memory: I advise you to call this method everytime you need the processor. */
191 public ImageProcessor getImageProcessor() {
192 final ImageProcessor ip = this.project.getLoader().fetchImageProcessor(this);
193 return ip;
196 /** Recreate mipmaps and flush away any cached ones.
197 * This method is essentially the same as patch.getProject().getLoader().update(patch);
198 * which in turn it's the same as the following two calls:
199 * patch.getProject().getLoader().generateMipMaps(patch);
200 * patch.getProject().getLoader().decacheAWT(patch.getId());
202 * If you want to update lots of Patch instances in parallel, consider also
203 * project.getLoader().generateMipMaps(ArrayList patches, boolean overwrite);
205 public boolean updateMipmaps() {
206 return project.getLoader().update(this);
209 private void readProps(final ImagePlus new_imp) {
210 this.type = new_imp.getType();
211 if (new_imp.getWidth() != (int)this.width || new_imp.getHeight() != this.height) {
212 this.width = new_imp.getWidth();
213 this.height = new_imp.getHeight();
214 updateBucket();
216 ImageProcessor ip = new_imp.getProcessor();
217 this.min = ip.getMin();
218 this.max = ip.getMax();
221 /** Set a new ImagePlus for this Patch.
222 * The original path and image remain untouched. Any later image is deleted and replaced by the new one.
224 synchronized public String set(final ImagePlus new_imp) {
225 if (null == new_imp) return null;
226 // flag to mean: this Patch has never been set to any image except the original
227 // The intention is never to remove the mipmaps of original images
228 boolean first_time = null == original_path;
229 // 0 - set original_path to the current path if there is no original_path recorded:
230 if (isStack()) {
231 for (Patch p : getStackPatches()) {
232 if (null == p.original_path) original_path = p.project.getLoader().getAbsolutePath(p);
234 } else {
235 if (null == original_path) original_path = project.getLoader().getAbsolutePath(this);
237 // 1 - tell the loader to store the image somewhere, unless the image has a path already
238 final String path = project.getLoader().setImageFile(this, new_imp);
239 if (null == path) {
240 Utils.log2("setImageFile returned null!");
241 return null; // something went wrong
243 // 2 - update properties and mipmaps
244 if (isStack()) {
245 for (Patch p : getStackPatches()) {
246 p.readProps(new_imp);
247 project.getLoader().generateMipMaps(p); // sequentially
248 project.getLoader().decacheAWT(p.id);
250 } else {
251 readProps(new_imp);
252 project.getLoader().generateMipMaps(this);
253 project.getLoader().decacheAWT(this.id);
255 Display.repaint(layer, this, 5);
256 return project.getLoader().getAbsolutePath(this);
259 /** Boundary checks on min and max, given the image type. */
260 private void checkMinMax() {
261 if (-1 == this.type) return;
262 switch (type) {
263 case ImagePlus.GRAY8:
264 case ImagePlus.COLOR_RGB:
265 case ImagePlus.COLOR_256:
266 if (this.min < 0) this.min = 0;
267 break;
269 final double max_max = Patch.getMaxMax(this.type);
270 if (this.max > max_max) this.max = max_max;
271 // still this.max could be -1, in which case putMinAndMax will fix it to the ImageProcessor's values
274 /** The min and max values are stored with the Patch, so that the image can be flushed away but the non-destructive contrast settings preserved. */
275 public void setMinAndMax(double min, double max) {
276 this.min = min;
277 this.max = max;
278 updateInDatabase("min_and_max");
279 Utils.log2("Patch.setMinAndMax: min,max " + min + "," + max);
282 public double getMin() { return min; }
283 public double getMax() { return max; }
285 /** Needs a non-null ImagePlus with a non-null ImageProcessor in it. This method is meant to be called only mmediately after the ImagePlus is loaded. */
286 public void putMinAndMax(final ImagePlus imp) throws Exception {
287 ImageProcessor ip = imp.getProcessor();
288 // adjust lack of values
289 if (-1 == min || -1 == max) {
290 min = ip.getMin();
291 max = ip.getMax();
292 } else {
293 ip.setMinAndMax(min, max);
295 //Utils.log2("Patch.putMinAndMax: min,max " + min + "," + max);
298 /** Returns the ImagePlus type of this Patch. */
299 public int getType() {
300 return type;
303 public Image createImage(ImagePlus imp) {
304 return adjustChannels(channels, true, imp);
307 public Image createImage() {
308 return adjustChannels(channels, true, null);
311 private Image adjustChannels(int c) {
312 return adjustChannels(c, false, null);
315 public int getChannelAlphas() {
316 return channels;
319 /** @param c contains the current Display 'channels' value (the transparencies of each channel). This method creates a new color image in which each channel (R, G, B) has the corresponding alpha (in fact, opacity) specified in the 'c'. This alpha is independent of the alpha of the whole Patch. The method updates the Loader cache with the newly created image. The argument 'imp' is optional: if null, it will be retrieved from the loader.<br />
320 * For non-color images, a standard image is returned regardless of the @param c
322 private Image adjustChannels(final int c, final boolean force, ImagePlus imp) {
323 if (null == imp) imp = project.getLoader().fetchImagePlus(this);
324 ImageProcessor ip = imp.getProcessor();
325 if (null == ip) return null; // fixing synch problems when deleting a Patch
326 Image awt = null;
327 if (ImagePlus.COLOR_RGB == type) {
328 if (imp.getType() != type ) {
329 ip = Utils.convertTo(ip, type, false); // all other types need not be converted, since there are no alphas anyway
331 if ((c&0x00ffffff) == 0x00ffffff && !force) {
332 // full transparency
333 awt = ip.createImage(); //imp.getImage();
334 // pixels array will be shared using ij138j and above
335 } else {
336 // modified from ij.process.ColorProcessor.createImage() by Wayne Rasband
337 int[] pixels = (int[])ip.getPixels();
338 float cr = ((c&0xff0000)>>16) / 255.0f;
339 float cg = ((c&0xff00)>>8) / 255.0f;
340 float cb = (c&0xff) / 255.0f;
341 int[] pix = new int[pixels.length];
342 int p;
343 for (int i=pixels.length -1; i>-1; i--) {
344 p = pixels[i];
345 pix[i] = (((int)(((p&0xff0000)>>16) * cr))<<16)
346 + (((int)(((p&0xff00)>>8) * cg))<<8)
347 + (int) ((p&0xff) * cb);
349 int w = imp.getWidth();
350 MemoryImageSource source = new MemoryImageSource(w, imp.getHeight(), DCM, pix, 0, w);
351 source.setAnimated(true);
352 source.setFullBufferUpdates(true);
353 awt = Toolkit.getDefaultToolkit().createImage(source);
355 } else {
356 awt = ip.createImage();
359 //Utils.log2("ip's min, max: " + ip.getMin() + ", " + ip.getMax());
361 this.channels = c;
363 return awt;
366 static final public DirectColorModel DCM = new DirectColorModel(24, 0xff0000, 0xff00, 0xff);
368 /** Just throws the cached image away if the alpha of the channels has changed. */
369 private final void checkChannels(int channels, double magnification) {
370 if (this.channels != channels && (ImagePlus.COLOR_RGB == this.type || ImagePlus.COLOR_256 == this.type)) {
371 final int old_channels = this.channels;
372 this.channels = channels; // before, so if any gets recreated it's done right
373 project.getLoader().adjustChannels(this, old_channels);
377 /** Takes an image and scales its channels according to the values packed in this.channels.
378 * This method is intended for fixing RGB images which are loaded from jpegs (the mipmaps), and which
379 * have then the full colorization of the original image present in their pixels array.
380 * Otherwise the channel opacity scaling makes no sense.
381 * If 0xffffffff == this.channels the awt is returned as is.
382 * If the awt is null returns null.
384 public final Image adjustChannels(final Image awt) {
385 if (0xffffffff == this.channels || null == awt) return awt;
386 BufferedImage bi = null;
387 // reuse if possible
388 if (awt instanceof BufferedImage) bi = (BufferedImage)awt;
389 else {
390 bi = new BufferedImage(awt.getWidth(null), awt.getHeight(null), BufferedImage.TYPE_INT_ARGB);
391 bi.getGraphics().drawImage(awt, 0, 0, null);
393 // extract channel values
394 final float cr = ((channels&0xff0000)>>16) / 255.0f;
395 final float cg = ((channels&0xff00)>>8 ) / 255.0f;
396 final float cb = ( channels&0xff ) / 255.0f;
397 // extract pixels
398 Utils.log2("w, h: " + bi.getWidth() + ", " + bi.getHeight());
399 final int[] pixels = bi.getRGB(0, 0, bi.getWidth(), bi.getHeight(), null, 0, 1);
400 // scale them according to channel opacities
401 int p;
402 for (int i=0; i<pixels.length; i++) {
403 p = pixels[i];
404 pixels[i] = (((int)(((p&0xff0000)>>16) * cr))<<16)
405 + (((int)(((p&0xff00)>>8) * cg))<<8)
406 + (int) ((p&0xff) * cb);
408 // replace pixels
409 bi.setRGB(0, 0, bi.getWidth(), bi.getHeight(), pixels, 0, 1);
410 return bi;
413 public void paint(Graphics2D g, double magnification, boolean active, int channels, Layer active_layer) {
415 AffineTransform atp = this.at;
417 checkChannels(channels, magnification);
419 // Consider all possible scaling components: m00, m01
420 // m10, m11
421 double sc = magnification * Math.max(Math.abs(at.getScaleX()),
422 Math.max(Math.abs(at.getScaleY()),
423 Math.max(Math.abs(at.getShearX()),
424 Math.abs(at.getShearY()))));
425 if (sc < 0) sc = magnification;
426 final Image image = project.getLoader().fetchImage(this, sc);
427 //Utils.log2("Patch " + id + " painted image " + image);
429 if (null == image) {
430 //Utils.log2("Patch.paint: null image, returning");
431 return; // TEMPORARY from lazy repaints after closing a Project
434 // fix dimensions: may be smaller or bigger mipmap than the image itself
435 final int iw = image.getWidth(null);
436 final int ih = image.getHeight(null);
437 if (iw != this.width || ih != this.height) {
438 atp = (AffineTransform)atp.clone();
439 atp.scale(this.width / iw, this.height / ih);
442 //arrange transparency
443 Composite original_composite = null;
444 if (alpha != 1.0f) {
445 original_composite = g.getComposite();
446 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
449 g.drawImage(image, atp, null);
451 //Transparency: fix composite back to original.
452 if (alpha != 1.0f) {
453 g.setComposite(original_composite);
458 /** Paint first whatever is available, then request that the proper image be loaded and painted. */
459 public void prePaint(final Graphics2D g, final double magnification, final boolean active, final int channels, final Layer active_layer) {
461 AffineTransform atp = this.at;
463 checkChannels(channels, magnification);
465 // Consider all possible scaling components: m00, m01
466 // m10, m11
467 double sc = magnification * Math.max(Math.abs(at.getScaleX()),
468 Math.max(Math.abs(at.getScaleY()),
469 Math.max(Math.abs(at.getShearX()),
470 Math.abs(at.getShearY()))));
471 if (sc < 0) sc = magnification;
473 Image image = project.getLoader().getCachedClosestAboveImage(this, sc); // above or equal
474 if (null == image) {
475 image = project.getLoader().getCachedClosestBelowImage(this, sc); // below, not equal
476 boolean thread = false;
477 if (null == image) {
478 // fetch the proper image, nothing is cached
479 if (sc <= 0.5001) {
480 // load the mipmap
481 image = project.getLoader().fetchImage(this, sc);
482 } else {
483 // load a smaller mipmap, and then load the larger one and repaint on load.
484 image = project.getLoader().fetchImage(this, 0.25);
485 thread = true;
487 // TODO to be non-blocking, this should paint a black square with a "loading..." legend in it or something, then fire a later repaint thread like below. So don't wait!
488 } else {
489 // painting a smaller image, will need to repaint with the proper one
490 thread = true;
492 if (thread && !Loader.NOT_FOUND.equals(image)) {
493 // use the lower resolution image, but ask to repaint it on load
494 Loader.preload(this, sc, true);
498 if (null == image) {
499 Utils.log2("Patch.paint: null image, returning");
500 return; // TEMPORARY from lazy repaints after closing a Project
503 // fix dimensions: may be smaller or bigger mipmap than the image itself
504 final int iw = image.getWidth(null);
505 final int ih = image.getHeight(null);
506 if (iw != this.width || ih != this.height) {
507 atp = (AffineTransform)atp.clone();
508 atp.scale(this.width / iw, this.height / ih);
511 //arrange transparency
512 Composite original_composite = null;
513 if (alpha != 1.0f) {
514 original_composite = g.getComposite();
515 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
518 g.drawImage(image, atp, null);
520 //Transparency: fix composite back to original.
521 if (null != original_composite) {
522 g.setComposite(original_composite);
526 /** A method to paint, simply (to a flat image for example); no magnification or srcRect are considered. */
527 public void paint(Graphics2D g) {
528 if (!this.visible) return;
530 Image image = project.getLoader().fetchImage(this); // TODO: could read the scale parameter of the graphics object and call for the properly sized mipmap accordingly.
532 //arrange transparency
533 Composite original_composite = null;
534 if (alpha != 1.0f) {
535 original_composite = g.getComposite();
536 g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
539 g.drawImage(image, this.at, null);
541 //Transparency: fix composite back to original.
542 if (alpha != 1.0f) {
543 g.setComposite(original_composite);
547 public boolean isDeletable() {
548 return 0 == width && 0 == height;
551 /** Remove only if linked to other Patches or to noone. */
552 public boolean remove(boolean check) {
553 if (check && !Utils.check("Really remove " + this.toString() + " ?")) return false;
554 if (isStack()) { // this Patch is part of a stack
555 GenericDialog gd = new GenericDialog("Stack!");
556 gd.addMessage("Really delete the entire stack?");
557 gd.addCheckbox("Delete layers if empty", true);
558 gd.showDialog();
559 if (gd.wasCanceled()) return false;
560 boolean delete_empty_layers = gd.getNextBoolean();
561 // gather all
562 HashMap<Double,Patch> ht = new HashMap<Double,Patch>();
563 getStackPatchesNR(ht);
564 Utils.log("Stack patches: " + ht.size());
565 ArrayList al = new ArrayList();
566 for (Iterator it = ht.values().iterator(); it.hasNext(); ) {
567 Patch p = (Patch)it.next();
568 if (!p.isOnlyLinkedTo(this.getClass())) {
569 Utils.showMessage("At least one slice of the stack (z=" + p.getLayer().getZ() + ") is supporting other data.\nCan't delete.");
570 return false;
573 for (Iterator it = ht.values().iterator(); it.hasNext(); ) {
574 Patch p = (Patch)it.next();
575 if (!p.layer.remove(p) || !p.removeFromDatabase()) {
576 Utils.showMessage("Can't delete Patch " + p);
577 return false;
579 p.unlink();
580 p.removeLinkedPropertiesFromOrigins();
581 //no need//it.remove();
582 al.add(p.layer);
583 if (p.layer.isEmpty()) Display.close(p.layer);
584 else Display.repaint(p.layer);
586 if (delete_empty_layers) {
587 for (Iterator it = al.iterator(); it.hasNext(); ) {
588 Layer la = (Layer)it.next();
589 if (la.isEmpty()) {
590 project.getLayerTree().remove(la, false);
591 Display.close(la);
595 Search.remove(this);
596 return true;
597 } else {
598 if (isOnlyLinkedTo(Patch.class, this.layer) && layer.remove(this) && removeFromDatabase()) { // don't alow to remove linked patches (unless only linked to other patches in the same layer)
599 unlink();
600 removeLinkedPropertiesFromOrigins();
601 Search.remove(this);
602 return true;
603 } else {
604 Utils.showMessage("Patch: can't remove! The image is linked and thus supports other data).");
605 return false;
610 /** Returns true if this Patch holds direct links to at least one other image in a different layer. Doesn't check for total overlap. */
611 public boolean isStack() {
612 if (null == hs_linked || hs_linked.isEmpty()) return false;
613 final Iterator it = hs_linked.iterator();
614 while (it.hasNext()) {
615 Displayable d = (Displayable)it.next();
616 if (d instanceof Patch && d.layer.getId() != this.layer.getId()) return true;
618 return false;
621 /** Retuns a virtual ImagePlus with a virtual stack if necessary. */
622 public PatchStack makePatchStack() {
623 // are we a stack?
624 HashMap<Double,Patch> ht = new HashMap<Double,Patch>();
625 getStackPatchesNR(ht);
626 Patch[] patch = null;
627 int currentSlice = 1; // from 1 to n, as in ImageStack
628 if (ht.size() > 1) {
629 // a stack. Order by layer Z
630 ArrayList<Double> z = new ArrayList<Double>();
631 z.addAll(ht.keySet());
632 java.util.Collections.sort(z);
633 patch = new Patch[z.size()];
634 int i = 0;
635 for (Double d : z) {
636 patch[i] = ht.get(d);
637 if (patch[i].id == this.id) currentSlice = i+1;
638 i++;
640 } else {
641 patch = new Patch[]{ this };
643 return new PatchStack(patch, currentSlice);
646 public ArrayList<Patch> getStackPatches() {
647 HashMap<Double,Patch> ht = new HashMap<Double,Patch>();
648 getStackPatchesNR(ht);
649 Utils.log2("Found patches: " + ht.size());
650 ArrayList<Double> z = new ArrayList<Double>();
651 z.addAll(ht.keySet());
652 java.util.Collections.sort(z);
653 ArrayList<Patch> p = new ArrayList<Patch>();
654 for (Double d : z) {
655 p.add(ht.get(d));
657 return p;
660 /** Collect linked Patch instances that do not lay in this layer. Recursive over linked Patch instances that lay in different layers. */ // This method returns a usable stack because Patch objects are only linked to other Patch objects when inserted together as stack. So the slices are all consecutive in space and have the same thickness. Yes this is rather convoluted, stacks should be full-grade citizens
661 private void getStackPatches(HashMap<Double,Patch> ht) {
662 if (ht.containsKey(this)) return;
663 ht.put(new Double(layer.getZ()), this);
664 if (null != hs_linked && hs_linked.size() > 0) {
666 for (Iterator it = hs_linked.iterator(); it.hasNext(); ) {
667 Displayable ob = (Displayable)it.next();
668 if (ob instanceof Patch && !ob.layer.equals(this.layer)) {
669 ((Patch)ob).getStackPatches(ht);
673 // avoid stack overflow (with as little as 114 layers ... !!!)
674 Displayable[] d = new Displayable[hs_linked.size()];
675 hs_linked.toArray(d);
676 for (int i=0; i<d.length; i++) {
677 if (d[i] instanceof Patch && d[i].layer.equals(this.layer)) {
678 ((Patch)d[i]).getStackPatches(ht);
684 /** Non-recursive version to avoid stack overflows with "excessive" recursion (I hate java). */
685 private void getStackPatchesNR(final HashMap<Double,Patch> ht) {
686 final ArrayList<Patch> list1 = new ArrayList<Patch>();
687 list1.add(this);
688 final ArrayList<Patch> list2 = new ArrayList<Patch>();
689 while (list1.size() > 0) {
690 list2.clear();
691 for (Patch p : list1) {
692 if (null != p.hs_linked) {
693 for (Iterator it = p.hs_linked.iterator(); it.hasNext(); ) {
694 Object ln = it.next();
695 if (ln instanceof Patch) {
696 Patch pa = (Patch)ln;
697 if (!ht.containsValue(pa)) {
698 ht.put(pa.layer.getZ(), pa);
699 list2.add(pa);
705 list1.clear();
706 list1.addAll(list2);
710 /** Opens and closes the tag and exports data. The image is saved in the directory provided in @param any as a String. */
711 public void exportXML(StringBuffer sb_body, String indent, Object any) { // TODO the Loader should handle the saving of images, not this class.
712 String in = indent + "\t";
713 String path = null;
714 String path2 = null;
715 //Utils.log2("#########\np id=" + id + " any is " + any);
716 if (null != any) {
717 path = any + title; // ah yes, automatic toString() .. it's like the ONLY smart logic at the object level built into java.
718 // save image without overwritting, and add proper extension (.zip)
719 path2 = project.getLoader().exportImage(this, path, false);
720 //Utils.log2("p id=" + id + " path2: " + path2);
721 // path2 will be null if the file exists already
723 sb_body.append(indent).append("<t2_patch\n");
724 String rel_path = null;
725 if (null != path && path.equals(path2)) { // this happens when a DB project is exported. It may be a different path when it's a FS loader
726 //Utils.log2("p id=" + id + " path==path2");
727 rel_path = path2;
728 int i_slash = rel_path.lastIndexOf(java.io.File.separatorChar);
729 if (i_slash > 0) {
730 i_slash = rel_path.lastIndexOf(java.io.File.separatorChar, i_slash -1);
731 if (-1 != i_slash) {
732 rel_path = rel_path.substring(i_slash+1);
735 } else {
736 //Utils.log2("Setting rel_path to " + path2);
737 rel_path = path2;
739 // For FSLoader projects, saving a second time will save images as null unless calling it
740 if (null == rel_path) {
741 //Utils.log2("path2 was null");
742 Object ob = project.getLoader().getPath(this);
743 path2 = null == ob ? null : (String)ob;
744 if (null == path2) {
745 //Utils.log2("ERROR: No path for Patch id=" + id + " and title: " + title);
746 rel_path = title; // at least some clue for recovery
747 } else {
748 rel_path = path2;
752 //Utils.log("Patch path is: " + rel_path);
754 super.exportXML(sb_body, in, any);
755 String[] RGB = Utils.getHexRGBColor(color);
756 int type = this.type;
757 if (-1 == this.type) {
758 Utils.log2("Retrieving type for p = " + this);
759 ImagePlus imp = project.getLoader().fetchImagePlus(this);
760 if (null != imp) type = imp.getType();
762 sb_body.append(in).append("type=\"").append(type /*null == any ? ImagePlus.GRAY8 : type*/).append("\"\n")
763 .append(in).append("file_path=\"").append(rel_path).append("\"\n")
764 .append(in).append("style=\"fill-opacity:").append(alpha).append(";stroke:#").append(RGB[0]).append(RGB[1]).append(RGB[2]).append(";\"\n")
765 .append(in).append("o_width=\"").append(o_width).append("\"\n")
766 .append(in).append("o_height=\"").append(o_height).append("\"\n")
768 if (null != original_path) {
769 sb_body.append(in).append("original_path=\"").append(original_path).append("\"\n");
771 if (0 != min) sb_body.append(in).append("min=\"").append(min).append("\"\n");
772 if (max != Patch.getMaxMax(type)) sb_body.append(in).append("max=\"").append(max).append("\"\n");
774 sb_body.append(indent).append(">\n");
776 if (null != ct) {
777 sb_body.append(ct.toXML(in)).append('\n');
780 super.restXML(sb_body, in, any);
782 sb_body.append(indent).append("</t2_patch>\n");
785 static private final double getMaxMax(final int type) {
786 int pow = 1;
787 switch (type) {
788 case ImagePlus.GRAY16: pow = 2; break; // TODO problems with unsigned short most likely
789 case ImagePlus.GRAY32: pow = 4; break;
790 default: return 255;
792 return Math.pow(256, pow) - 1;
795 static public void exportDTD(StringBuffer sb_header, HashSet hs, String indent) {
796 String type = "t2_patch";
797 if (hs.contains(type)) return;
798 // The Patch itself:
799 sb_header.append(indent).append("<!ELEMENT t2_patch (").append(Displayable.commonDTDChildren()).append(",ict_transform,ict_transform_list)>\n");
800 Displayable.exportDTD(type, sb_header, hs, indent);
801 sb_header.append(indent).append(TAG_ATTR1).append(type).append(" file_path").append(TAG_ATTR2)
802 .append(indent).append(TAG_ATTR1).append(type).append(" original_path").append(TAG_ATTR2)
803 .append(indent).append(TAG_ATTR1).append(type).append(" type").append(TAG_ATTR2)
804 .append(indent).append(TAG_ATTR1).append(type).append(" ct").append(TAG_ATTR2)
805 .append(indent).append(TAG_ATTR1).append(type).append(" o_width").append(TAG_ATTR2)
806 .append(indent).append(TAG_ATTR1).append(type).append(" o_height").append(TAG_ATTR2)
808 // The InvertibleCoordinateTransform and a list of:
809 sb_header.append(indent).append("<!ELEMENT ict_transform EMPTY>\n");
810 sb_header.append(indent).append(TAG_ATTR1).append("ict_transform class").append(TAG_ATTR2)
811 .append(indent).append(TAG_ATTR1).append("ict_transform data").append(TAG_ATTR2);
812 sb_header.append(indent).append("<!ELEMENT ict_transform_list (ict_transform)>\n");
816 /** Performs a copy of this object, without the links, unlocked and visible, except for the image which is NOT duplicated. If the project is NOT the same as this instance's project, then the id of this instance gets assigned as well to the returned clone. */
817 public Displayable clone(final Project pr, final boolean copy_id) {
818 final long nid = copy_id ? this.id : pr.getLoader().getNextId();
819 final Patch copy = new Patch(pr, nid, null != title ? title.toString() : null, width, height, type, false, min, max, (AffineTransform)at.clone());
820 copy.color = new Color(color.getRed(), color.getGreen(), color.getBlue());
821 copy.alpha = this.alpha;
822 copy.visible = true;
823 copy.channels = this.channels;
824 copy.min = this.min;
825 copy.max = this.max;
826 copy.ct = null == ct ? null : this.ct.clone();
827 copy.addToDatabase();
828 pr.getLoader().addedPatchFrom(this.project.getLoader().getAbsolutePath(this), copy);
829 copy.setAlphaMask(this.project.getLoader().fetchImageMask(this));
830 return copy;
833 /** Override to cancel. */
834 public void linkPatches() {
835 Utils.log2("Patch class can't link other patches using Displayble.linkPatches()");
838 public void paintSnapshot(final Graphics2D g, final double mag) {
839 switch (layer.getParent().getSnapshotsMode()) {
840 case 0:
841 if (!project.getLoader().isSnapPaintable(this.id)) {
842 paintAsBox(g);
843 } else {
844 paint(g, mag, false, this.channels, layer);
846 return;
847 case 1:
848 paintAsBox(g);
849 return;
850 default: return; // case 2: // disabled, no paint
854 static protected void crosslink(final ArrayList patches, final boolean overlapping_only) {
855 if (null == patches) return;
856 final ArrayList<Patch> al = new ArrayList<Patch>();
857 for (Object ob : patches) if (ob instanceof Patch) al.add((Patch)ob); // ...
858 final int len = al.size();
859 if (len < 2) return;
860 final Patch[] pa = new Patch[len];
861 al.toArray(pa);
862 // linking is reciprocal: need only call link() on one member of the pair
863 for (int i=0; i<pa.length; i++) {
864 for (int j=i+1; j<pa.length; j++) {
865 if (overlapping_only && !pa[i].intersects(pa[j])) continue;
866 pa[i].link(pa[j]);
871 /** Magnification-dependent counterpart to ImageProcessor.getPixel(x, y). Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method.*/
872 public int getPixel(double mag, final int x, final int y) {
873 final int[] iArray = getPixel(x, y, mag);
874 if (ImagePlus.COLOR_RGB == this.type) {
875 return (iArray[0]<<16) + (iArray[1]<<8) + iArray[2];
877 return iArray[0];
880 /** Magnification-dependent counterpart to ImageProcessor.getPixel(x, y, iArray). Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method.*/
881 public int[] getPixel(double mag, final int x, final int y, final int[] iArray) {
882 final int[] ia = getPixel(x, y, mag);
883 if(null != iArray) {
884 iArray[0] = ia[0];
885 iArray[1] = ia[1];
886 iArray[2] = ia[2];
887 return iArray;
889 return ia;
892 /** Expects x,y in world coordinates. This method is intended for grabing an occasional pixel; to grab all pixels, see @getImageProcessor method. */
893 public int[] getPixel(final int x, final int y, final double mag) {
894 if (project.getLoader().isUnloadable(this)) return new int[4];
895 final Image img = project.getLoader().fetchImage(this, mag);
896 if (Loader.isSignalImage(img)) return new int[4];
897 final int w = img.getWidth(null);
898 final double scale = w / width;
899 final Point2D.Double pd = inverseTransformPoint(x, y);
900 final int x2 = (int)(pd.x * scale);
901 final int y2 = (int)(pd.y * scale);
902 final int[] pvalue = new int[4];
903 final PixelGrabber pg = new PixelGrabber(img, x2, y2, 1, 1, pvalue, 0, w);
904 try {
905 pg.grabPixels();
906 } catch (InterruptedException ie) {
907 return pvalue;
909 switch (type) {
910 case ImagePlus.COLOR_256:
911 final PixelGrabber pg2 = new PixelGrabber(img,x2,y2,1,1,false);
912 try {
913 pg2.grabPixels();
914 } catch (InterruptedException ie) {
915 return pvalue;
917 final byte[] pix8 = (byte[])pg2.getPixels();
918 pvalue[3] = null != pix8 ? pix8[0]&0xff : 0;
919 // fall through to get RGB values
920 case ImagePlus.COLOR_RGB:
921 final int c = pvalue[0];
922 pvalue[0] = (c&0xff0000)>>16; // R
923 pvalue[1] = (c&0xff00)>>8; // G
924 pvalue[2] = c&0xff; // B
925 break;
926 case ImagePlus.GRAY8:
927 pvalue[0] = pvalue[0]&0xff;
928 break;
929 default: // all others: GRAY16, GRAY32
930 pvalue[0] = pvalue[0]&0xff;
931 // correct range: from 8-bit of the mipmap to 16 or 32 bit
932 if (mag <= 0.5) {
933 // mipmap was an 8-bit image, so expand
934 pvalue[0] = (int)(min + pvalue[0] * ( (max - min) / 256 ));
936 break;
939 return pvalue;
942 /** If this patch is part of a stack, the file path will contain the slice number attached to it, in the form -----#slice=10 for slice number 10. */
943 public final String getFilePath() {
944 if (null != current_path) return current_path;
945 return project.getLoader().getAbsolutePath(this);
948 /** Returns the absolute path to the image file, as read by the OS. */
949 public final String getImageFilePath() {
950 return project.getLoader().getAbsoluteFilePath(this);
953 /** Returns the value of the field current_path, which may be null. If not null, the value may contain the slice info in it if it's part of a stack. */
954 public final String getCurrentPath() { return current_path; }
956 /** Cache a proper, good, known path to the image wrapped by this Patch. */
957 public final void cacheCurrentPath(final String path) {
958 this.current_path = path;
961 /** Returns the value of the field original_path, which may be null. If not null, the value may contain the slice info in it if it's part of a stack. */
962 synchronized public String getOriginalPath() { return original_path; }
964 protected void setAlpha(float alpha, boolean update) {
965 if (isStack()) {
966 HashMap<Double,Patch> ht = new HashMap<Double,Patch>();
967 getStackPatchesNR(ht);
968 for (Patch pa : ht.values()) {
969 pa.alpha = alpha;
970 pa.updateInDatabase("alpha");
971 Display.repaint(pa.layer, pa, 5);
973 Display3D.setTransparency(this, alpha);
974 } else super.setAlpha(alpha, update);
977 public void debug() {
978 Utils.log2("Patch id=" + id + "\n\toriginal_path=" + original_path + "\n\tcurrent_path=" + current_path);
981 /** Revert the ImagePlus to the one stored in original_path, if any; will revert all linked patches if this is part of a stack. */
982 synchronized public boolean revert() {
983 if (null == original_path) return false; // nothing to revert to
984 // 1 - check that original_path exists
985 if (!new File(original_path).exists()) {
986 Utils.log("CANNOT revert: Original file path does not exist: " + original_path + " for patch " + getTitle() + " #" + id);
987 return false;
989 // 2 - check that the original can be loaded
990 final ImagePlus imp = project.getLoader().fetchOriginal(this);
991 if (null == imp || null == set(imp)) {
992 Utils.log("CANNOT REVERT: original image at path " + original_path + " fails to load, for patch " + getType() + " #" + id);
993 return false;
995 // 3 - update path in loader, and cache imp for each stack slice id
996 if (isStack()) {
997 for (Patch p : getStackPatches()) {
998 p.project.getLoader().addedPatchFrom(p.original_path, p);
999 p.project.getLoader().cacheImagePlus(p.id, imp);
1000 p.project.getLoader().generateMipMaps(p);
1002 } else {
1003 project.getLoader().addedPatchFrom(original_path, this);
1004 project.getLoader().cacheImagePlus(id, imp);
1005 project.getLoader().generateMipMaps(this);
1007 // 4 - update screens
1008 Display.repaint(layer, this, 0);
1009 Utils.showStatus("Reverted patch " + getTitle(), false);
1010 return true;
1013 /** For reconstruction purposes, overwrites the present CoordinateTransform, if any, with the given one. */
1014 public void setCoordinateTransformSilently(final CoordinateTransform ct) {
1015 this.ct = ct;
1018 /** Set a CoordinateTransform to this Patch.
1019 * The resulting image of applying the coordinate transform does not need to be rectangular: an alpha mask will take care of the borders. You should call updateMipmaps() afterwards to update the mipmap images used for painting this Patch to the screen. */
1020 public final void setCoordinateTransform(final CoordinateTransform ct) {
1021 if (isLinked()) {
1022 Utils.log("Cannot set coordinate transform: patch is linked!");
1023 return;
1026 if (null != this.ct) {
1027 // restore image without the transform
1028 final TransformMesh mesh = new TransformMesh(this.ct, 32, o_width, o_height);
1029 final Rectangle box = mesh.getBoundingBox();
1030 this.at.translate(-box.x, -box.y);
1031 updateInDatabase("transform+dimensions");
1034 this.ct = ct;
1035 updateInDatabase("ict_transform");
1037 if (null == this.ct) {
1038 width = o_width;
1039 height = o_height;
1040 updateBucket();
1041 return;
1044 // Adjust the AffineTransform to correct for bounding box displacement
1046 final TransformMesh mesh = new TransformMesh(this.ct, 32, o_width, o_height);
1047 final Rectangle box = mesh.getBoundingBox();
1048 this.at.translate(box.x, box.y);
1049 this.width = box.width;
1050 this.height = box.height;
1051 updateInDatabase("transform+dimensions"); // the AffineTransform
1052 updateBucket();
1054 // Updating the mipmaps will call createTransformedImage below if ct is not null
1055 /* DISABLED */ //updateMipmaps();
1059 * Append a {@link CoordinateTransform} to the current
1060 * {@link CoordinateTransformList}. If there is no transform yet, it just
1061 * sets it. If there is only one transform, it replaces it by a list
1062 * containing both.
1064 public final void appendCoordinateTransform(final CoordinateTransform ct) {
1065 if (null == this.ct)
1066 setCoordinateTransform(ct);
1067 else {
1068 final CoordinateTransformList ctl;
1069 if (this.ct instanceof CoordinateTransformList)
1070 ctl = (CoordinateTransformList)this.ct;
1071 else {
1072 ctl = new CoordinateTransformList();
1073 ctl.add(this.ct);
1075 ctl.add(ct);
1076 setCoordinateTransform(ctl);
1081 * Get the bounding rectangle of the transformed image relative to the
1082 * original image.
1084 * TODO
1085 * Currently, this is done in a very expensive way. The
1086 * {@linkplain TransformMesh} is built and its bounding rectangle is
1087 * returned. Think about just storing this rectangle in the
1088 * {@linkplain Patch} instance.
1090 * @return
1092 public final Rectangle getCoordinateTransformBoundingBox() {
1093 if (null==ct)
1094 return new Rectangle(0,0,o_width,o_height);
1095 final TransformMesh mesh = new TransformMesh(this.ct, 32, o_width, o_height);
1096 return mesh.getBoundingBox();
1099 public final CoordinateTransform getCoordinateTransform() { return ct; }
1101 public final Patch.PatchImage createCoordinateTransformedImage() {
1102 if (null == ct) return null;
1104 project.getLoader().releaseToFit(o_width, o_height, type, 5);
1106 final ImageProcessor source = getImageProcessor();
1108 //Utils.log2("source image dimensions: " + source.getWidth() + ", " + source.getHeight());
1110 final TransformMesh mesh = new TransformMesh(ct, 32, o_width, o_height);
1111 final TransformMeshMapping mapping = new TransformMeshMapping( mesh );
1113 ImageProcessor target = mapping.createMappedImageInterpolated( source );
1115 ByteProcessor outside = new ByteProcessor( source.getWidth(), source.getHeight() );
1116 outside.setValue(255);
1117 outside.fill();
1119 outside = (ByteProcessor) mapping.createMappedImageInterpolated( outside );
1121 ByteProcessor mask = project.getLoader().fetchImageMask(this);
1122 if (null != mask)
1123 mask = (ByteProcessor) mapping.createMappedImageInterpolated( mask );
1125 // Set all non-white pixels to zero
1126 final byte[] pix = (byte[])outside.getPixels();
1127 for (int i=0; i<pix.length; i++)
1128 if ((pix[i]&0xff) != 255) pix[i] = 0;
1130 final Rectangle box = mesh.getBoundingBox();
1132 //Utils.log2("New image dimensions: " + target.getWidth() + ", " + target.getHeight());
1133 //Utils.log2("box: " + box);
1135 return new PatchImage( target, mask, outside, box, true );
1138 public final class PatchImage {
1139 /** The image, coordinate-transformed if null != ct. */
1140 final public ImageProcessor target;
1141 /** The alpha mask, coordinate-transformed if null != ct. */
1142 final public ByteProcessor mask;
1143 /** The outside mask, coordinate-transformed if null != ct. */
1144 final public ByteProcessor outside;
1145 /** The bounding box of the image relative to the original, with x,y as the displacement relative to the pixels of the original image. */
1146 final public Rectangle box;
1147 /** Whether the image was generated with a CoordinateTransform or not. */
1148 final public boolean coordinate_transformed;
1150 private PatchImage( ImageProcessor target, ByteProcessor mask, ByteProcessor outside, Rectangle box, boolean coordinate_transformed ) {
1151 this.target = target;
1152 this.mask = mask;
1153 this.outside = outside;
1154 this.box = box;
1155 this.coordinate_transformed = coordinate_transformed;
1159 /** Returns a PatchImage object containing the bottom-of-transformation-stack image and alpha mask, if any (except the AffineTransform, which is used for direct hw-accel screen rendering). */
1160 public Patch.PatchImage createTransformedImage() {
1161 final Patch.PatchImage pi = createCoordinateTransformedImage();
1162 if (null != pi) return pi;
1163 // else, a new one with the untransformed, original image (a duplicate):
1164 project.getLoader().releaseToFit(o_width, o_height, type, 3);
1165 final ImageProcessor ip = getImageProcessor();
1166 if (null == ip) return null;
1167 return new PatchImage(ip.duplicate(), project.getLoader().fetchImageMask(this), null, new Rectangle(0, 0, o_width, o_height), false);
1170 private boolean has_alpha = false;
1171 private boolean alpha_path_checked = false;
1173 /** Caching system to avoid repeated checks. No automatic memoization ... snif */
1174 private final boolean hasMask() {
1175 if (alpha_path_checked) return has_alpha;
1176 // else, see if the path exists:
1177 try {
1178 has_alpha = new File(project.getLoader().getAlphaPath(this)).exists();
1179 } catch (Exception e) {
1180 IJError.print(e);
1182 alpha_path_checked = true;
1183 return has_alpha;
1186 public boolean hasAlphaChannel() {
1187 return null != ct || hasMask();
1190 /** Must call updateMipmaps() afterwards. Set it to null to remove it. */
1191 public void setAlphaMask(ByteProcessor bp) throws IllegalArgumentException {
1192 if (null == bp) {
1193 if (hasMask()) {
1194 if (project.getLoader().removeAlphaMask(this)) {
1195 alpha_path_checked = false;
1198 return;
1201 if (o_width != bp.getWidth() || o_height != bp.getHeight()) {
1202 throw new IllegalArgumentException("Need a mask of identical dimensions as the original image.");
1204 project.getLoader().storeAlphaMask(this, bp);
1205 alpha_path_checked = false;
1208 public void keyPressed(KeyEvent ke) {
1209 Object source = ke.getSource();
1210 if (! (source instanceof DisplayCanvas)) return;
1211 DisplayCanvas dc = (DisplayCanvas)source;
1212 final Layer la = dc.getDisplay().getLayer();
1213 final Roi roi = dc.getFakeImagePlus().getRoi();
1215 switch (ke.getKeyCode()) {
1216 case KeyEvent.VK_C:
1217 // copy into ImageJ clipboard
1218 int mod = ke.getModifiers();
1220 // Ignoring masks: outside is already black, and ImageJ cannot handle alpha masks.
1221 if (0 == mod || (0 == (mod ^ Event.SHIFT_MASK))) {
1222 CoordinateTransformList list = null;
1223 if (null != ct) {
1224 list = new CoordinateTransformList();
1225 list.add(this.ct);
1227 if (0 == mod) { //SHIFT is down
1228 AffineModel2D am = new AffineModel2D();
1229 am.set(this.at);
1230 if (null == list) list = new CoordinateTransformList();
1231 list.add(am);
1233 ImageProcessor ip;
1234 if (null != list) {
1235 TransformMesh mesh = new TransformMesh(list, 32, o_width, o_height);
1236 TransformMeshMapping mapping = new TransformMeshMapping(mesh);
1237 ip = mapping.createMappedImageInterpolated(getImageProcessor());
1238 } else {
1239 ip = getImageProcessor();
1241 new ImagePlus(this.title, ip).copy(false);
1242 } else if (0 == (mod ^ (Event.SHIFT_MASK | Event.ALT_MASK))) {
1243 // On shift down (and no other flags!):
1244 // Place the source image, untransformed, into clipboard:
1245 ImagePlus imp = getImagePlus();
1246 if (null != imp) imp.copy(false);
1248 ke.consume();
1249 break;
1250 case KeyEvent.VK_F:
1251 // fill mask with current ROI using
1252 Utils.log2("VK_F: roi is " + roi);
1253 if (null != roi && Utils.isAreaROI(roi)) {
1254 Bureaucrat.createAndStart(new Worker("Filling image mask") { public void run() { try {
1255 startedWorking();
1256 ByteProcessor mask = project.getLoader().fetchImageMask(Patch.this);
1257 boolean is_new = false;
1258 if (null == mask) {
1259 mask = new ByteProcessor(o_width, o_height);
1260 mask.setValue(255);
1261 mask.fill();
1262 is_new = true;
1264 try {
1265 // a roi local to the image bounding box
1266 final Area a = new Area(new Rectangle(0, 0, (int)width, (int)height));
1267 a.intersect(Utils.getArea(roi).createTransformedArea(Patch.this.at.createInverse()));
1269 if (Utils.isEmpty(a)) {
1270 Utils.log("ROI does not intersect the active image!");
1271 return;
1274 if (null != ct) {
1275 // inverse the coordinate transform
1276 final TransformMesh mesh = new TransformMesh(ct, 32, o_width, o_height);
1277 final TransformMeshMapping mapping = new TransformMeshMapping( mesh );
1279 ByteProcessor rmask = new ByteProcessor((int)width, (int)height);
1281 if (is_new) {
1282 rmask.setColor(Toolbar.getForegroundColor());
1283 } else {
1284 rmask.setValue(255);
1286 ShapeRoi sroi = new ShapeRoi(a);
1287 rmask.setRoi(sroi);
1288 rmask.fill(sroi.getMask());
1290 ByteProcessor inv_mask = (ByteProcessor) mapping.createInverseMappedImageInterpolated(rmask);
1292 if (is_new) {
1293 mask = inv_mask;
1294 // done!
1295 } else {
1296 // Blend
1297 rmask = null;
1298 inv_mask.setMinAndMax(255, 255);
1299 final byte[] b1 = (byte[]) mask.getPixels();
1300 final byte[] b2 = (byte[]) inv_mask.getPixels();
1301 final int color = mask.getBestIndex(Toolbar.getForegroundColor());
1302 for (int i=0; i<b1.length; i++) {
1303 b1[i] = (byte) ((int)( (b2[i] & 0xff) / 255.0f ) * (color - (b1[i] & 0xff) ) + (b1[i] & 0xff));
1306 } else {
1307 ShapeRoi sroi = new ShapeRoi(a);
1308 mask.setRoi(sroi);
1309 mask.setColor(Toolbar.getForegroundColor());
1310 mask.fill(sroi.getMask());
1312 } catch (NoninvertibleTransformException nite) { IJError.print(nite); }
1313 setAlphaMask(mask);
1314 updateMipmaps();
1315 Display.repaint();
1316 } catch (Exception e) {
1317 IJError.print(e);
1318 } finally {
1319 finishedWorking();
1320 }}}, project);
1322 // capturing:
1323 ke.consume();
1324 break;
1328 @Override
1329 Class getInternalDataPackageClass() {
1330 return DPPatch.class;
1333 @Override
1334 Object getDataPackage() {
1335 return new DPPatch(this);
1338 static private final class DPPatch extends Displayable.DataPackage {
1339 final double min, max;
1340 CoordinateTransform ct = null;
1342 DPPatch(final Patch patch) {
1343 super(patch);
1344 this.min = patch.min;
1345 this.max = patch.max;
1346 this.ct = null == ct ? null : patch.ct.clone();
1347 // channels is visualization
1348 // path is absolute
1349 // type is dependent on path, so absolute
1350 // o_width, o_height idem
1352 final boolean to2(final Displayable d) {
1353 super.to1(d);
1354 final Patch p = (Patch) d;
1355 boolean mipmaps = false;
1356 if (p.min != min || p.max != max || p.ct != ct || (p.ct == ct && ct instanceof CoordinateTransformList)) {
1357 Utils.log2("mipmaps is true! " + (p.min != min) + " " + (p.max != max) + " " + (p.ct != ct) + " " + (p.ct == ct && ct instanceof CoordinateTransformList));
1358 mipmaps = true;
1360 p.min = min;
1361 p.max = max;
1362 p.ct = null == ct ? null : (CoordinateTransform) ct.clone();
1364 if (mipmaps) {
1365 Utils.log2("Update mipmaps in a background task");
1366 ArrayList al = new ArrayList();
1367 al.add(p);
1368 p.project.getLoader().generateMipMaps(al, true);
1370 return true;