Merge branch 'undo-system' into ict
[trakem2.git] / ini / trakem2 / persistence / FSLoader.java
blob6f5a79179f262d2c1b11b13f97df1f07211ad8c6
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 /s 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.persistence;
25 import ij.IJ;
26 import ij.ImagePlus;
27 import ij.VirtualStack;
28 import ij.io.*;
29 import ij.process.ByteProcessor;
30 import ij.process.ImageProcessor;
31 import ij.process.FloatProcessor;
32 import ij.process.ColorProcessor;
33 import ini.trakem2.Project;
34 import ini.trakem2.ControlWindow;
35 import ini.trakem2.display.DLabel;
36 import ini.trakem2.display.Display;
37 import ini.trakem2.display.Layer;
38 import ini.trakem2.display.Patch;
39 import ini.trakem2.display.YesNoDialog;
40 import ini.trakem2.utils.*;
41 import ini.trakem2.io.*;
42 import ini.trakem2.imaging.FloatProcessorT2;
44 import java.awt.Graphics2D;
45 import java.awt.Image;
46 import java.awt.image.BufferedImage;
47 import java.awt.image.IndexColorModel;
48 import java.awt.image.ColorModel;
49 import java.awt.image.PixelGrabber;
50 import java.awt.RenderingHints;
51 import java.awt.geom.Area;
52 import java.awt.geom.AffineTransform;
53 import java.io.BufferedInputStream;
54 import java.io.File;
55 import java.io.FileInputStream;
56 import java.io.FilenameFilter;
57 import java.io.InputStream;
58 import java.util.*;
60 import javax.swing.JMenuItem;
61 import javax.swing.JMenu;
62 import java.awt.event.ActionListener;
63 import java.awt.event.ActionEvent;
64 import java.awt.event.KeyEvent;
65 import javax.swing.KeyStroke;
67 import org.xml.sax.InputSource;
69 import javax.xml.parsers.SAXParserFactory;
70 import javax.xml.parsers.SAXParser;
72 import mpi.fruitfly.math.datastructures.FloatArray2D;
73 import mpi.fruitfly.registration.ImageFilter;
74 import mpi.fruitfly.general.MultiThreading;
76 import java.util.concurrent.atomic.AtomicInteger;
79 /** A class to rely on memory only; except images which are rolled from a folder or their original location and flushed when memory is needed for more. Ideally there would be a given folder for storing items temporarily of permanently as the "project folder", but I haven't implemented it. */
80 public final class FSLoader extends Loader {
82 /** Largest id seen so far. */
83 private long max_id = -1;
84 private final HashMap<Long,String> ht_paths = new HashMap<Long,String>();
85 /** For saving and overwriting. */
86 private String project_file_path = null;
87 /** Path to the directory hosting the file image pyramids. */
88 private String dir_mipmaps = null;
89 /** Path to the directory the user provided when creating the project. */
90 private String dir_storage = null;
91 /** Path to the directory hosting the alpha masks. */
92 private String dir_masks = null;
94 /** Path to dir_storage + "trakem2.images/" */
95 private String dir_image_storage = null;
97 /** Queue and execute Runnable tasks. */
98 static private Dispatcher dispatcher = new Dispatcher();
100 private Set<Patch> touched_mipmaps = Collections.synchronizedSet(new HashSet<Patch>());
102 /** Used to open a project from an existing XML file. */
103 public FSLoader() {
104 super(); // register
105 super.v_loaders.remove(this); //will be readded on successful open
108 /** Used to create a new project, NOT from an XML file. */
109 public FSLoader(final String storage_folder) {
110 this();
111 if (null == storage_folder) this.dir_storage = super.getStorageFolder(); // home dir
112 else this.dir_storage = storage_folder;
113 if (!this.dir_storage.endsWith("/")) this.dir_storage += "/";
114 if (!Loader.canReadAndWriteTo(dir_storage)) {
115 Utils.log("WARNING can't read/write to the storage_folder at " + dir_storage);
116 } else {
117 createMipMapsDir(this.dir_storage);
118 crashDetector();
122 /** Create a new FSLoader copying some key parameters such as preprocessor plugin, and storage and mipmap folders. Used for creating subprojects. */
123 public FSLoader(final Loader source) {
124 this();
125 this.dir_storage = source.getStorageFolder(); // can never be null
126 this.dir_mipmaps = source.getMipMapsFolder();
127 if (null == this.dir_mipmaps) createMipMapsDir(this.dir_storage);
128 setPreprocessor(source.getPreprocessor());
131 /** Store a hidden file in trakem2.mipmaps directory that means: "the project is open", which is deleted when the project is closed. If the file is present on opening a project, it means the project has not been closed properly, and some mipmaps may be wrong. */
132 private void crashDetector() {
133 if (null == dir_mipmaps) {
134 Utils.log2("Could NOT create crash detection system: null dir_mipmaps.");
135 return;
137 File f = new File(dir_mipmaps + ".open.t2");
138 Utils.log2("Crash detector file is " + dir_mipmaps + ".open.t2");
139 try {
140 if (f.exists()) {
141 // crashed!
142 askAndExecMipmapRegeneration("TrakEM detected a crash!");
143 } else {
144 if (!f.createNewFile() && !dir_mipmaps.startsWith("http:")) {
145 Utils.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
146 } else {
147 Utils.log2("Created crash detection system.");
150 } catch (Exception e) {
151 Utils.log2("Crash detector error:" + e);
152 IJError.print(e);
156 public String getProjectXMLPath() {
157 if (null == project_file_path) return null;
158 return project_file_path.toString(); // a copy of it
161 public String getStorageFolder() {
162 if (null == dir_storage) return super.getStorageFolder(); // the user's home
163 return dir_storage.toString(); // a copy
166 /** Returns a folder proven to be writable for images can be stored into. */
167 public String getImageStorageFolder() {
168 if (null == dir_image_storage) {
169 String s = getStorageFolder() + "trakem2.images/";
170 File f = new File(s);
171 if (f.exists() && f.isDirectory() && f.canWrite()) {
172 dir_image_storage = s;
173 return dir_image_storage;
175 else {
176 try {
177 f.mkdirs();
178 dir_image_storage = s;
179 } catch (Exception e) {
180 e.printStackTrace();
181 return getStorageFolder(); // fall back
185 return dir_image_storage;
188 /** Returns TMLHandler.getProjectData() . If the path is null it'll be asked for. */
189 public Object[] openFSProject(String path, final boolean open_displays) {
190 // clean path of double-slashes, safely (and painfully)
191 if (null != path) {
192 path = path.replace('\\','/');
193 path = path.trim();
194 int itwo = path.indexOf("//");
195 while (-1 != itwo) {
196 if (0 == itwo /* samba disk */
197 || (5 == itwo && "http:".equals(path.substring(0, 5)))) {
198 // do nothing
199 } else {
200 path = path.substring(0, itwo) + path.substring(itwo+1);
202 itwo = path.indexOf("//", itwo+1);
206 if (null == path) {
207 String user = System.getProperty("user.name");
208 OpenDialog od = new OpenDialog("Select Project", OpenDialog.getDefaultDirectory(), null);
209 String file = od.getFileName();
210 if (null == file || file.toLowerCase().startsWith("null")) return null;
211 String dir = od.getDirectory().replace('\\', '/');
212 if (!dir.endsWith("/")) dir += "/";
213 this.project_file_path = dir + file;
214 Utils.log2("project file path 1: " + this.project_file_path);
215 } else {
216 this.project_file_path = path;
217 Utils.log2("project file path 2: " + this.project_file_path);
219 Utils.log2("Loader.openFSProject: path is " + path);
220 // check if any of the open projects uses the same file path, and refuse to open if so:
221 if (null != FSLoader.getOpenProject(project_file_path, this)) {
222 Utils.showMessage("The project is already open.");
223 return null;
226 Object[] data = null;
228 // parse file, according to expected format as indicated by the extension:
229 if (this.project_file_path.toLowerCase().endsWith(".xml")) {
230 InputStream i_stream = null;
231 TMLHandler handler = new TMLHandler(this.project_file_path, this);
232 if (handler.isUnreadable()) {
233 handler = null;
234 } else {
235 try {
236 SAXParserFactory factory = SAXParserFactory.newInstance();
237 factory.setValidating(true);
238 SAXParser parser = factory.newSAXParser();
239 if (isURL(this.project_file_path)) {
240 i_stream = new java.net.URL(this.project_file_path).openStream();
241 } else {
242 i_stream = new BufferedInputStream(new FileInputStream(this.project_file_path));
244 InputSource input_source = new InputSource(i_stream);
245 setMassiveMode(true);
246 parser.parse(input_source, handler);
247 } catch (java.io.FileNotFoundException fnfe) {
248 Utils.log("ERROR: File not found: " + path);
249 handler = null;
250 } catch (Exception e) {
251 IJError.print(e);
252 handler = null;
253 } finally {
254 setMassiveMode(false);
255 if (null != i_stream) {
256 try {
257 i_stream.close();
258 } catch (Exception e) {
259 IJError.print(e);
264 if (null == handler) {
265 Utils.showMessage("Error when reading the project .xml file.");
266 return null;
269 data = handler.getProjectData(open_displays);
272 if (null == data) {
273 Utils.showMessage("Error when parsing the project .xml file.");
274 return null;
276 // else, good
277 super.v_loaders.add(this);
278 crashDetector();
279 return data;
282 // Only one thread at a time may access this method.
283 synchronized static private final Project getOpenProject(final String project_file_path, final Loader caller) {
284 if (null == v_loaders) return null;
285 final Loader[] lo = (Loader[])v_loaders.toArray(new Loader[0]); // atomic way to get the list of loaders
286 for (int i=0; i<lo.length; i++) {
287 if (lo[i].equals(caller)) continue;
288 if (lo[i] instanceof FSLoader && ((FSLoader)lo[i]).project_file_path.equals(project_file_path)) {
289 return Project.findProject(lo[i]);
292 return null;
295 static public final Project getOpenProject(final String project_file_path) {
296 return getOpenProject(project_file_path, null);
299 public boolean isReady() {
300 return null != ht_paths;
303 public void destroy() {
304 super.destroy();
305 Utils.showStatus("", false);
306 // delete mipmap files that where touched and not cleared as saved (i.e. the project was not saved)
307 for (final Patch p : touched_mipmaps) {
308 File f = new File(getAbsolutePath(p));
309 Utils.log2("File f is " + f);
310 if (f.exists()) {
311 Utils.log2("Removing mipmaps for " + p);
312 // Cannot run in the dispatcher: is a daemon, and would be interrupted.
313 removeMipMaps(f.getName() + "." + p.getId() + ".jpg", (int)p.getWidth(), (int)p.getHeight()); // needs the dispatcher!
317 dispatcher.quit();
318 // remove empty trakem2.mipmaps folder if any
319 if (null != dir_mipmaps && !dir_mipmaps.equals(dir_storage)) {
320 File f = new File(dir_mipmaps);
321 if (f.isDirectory() && 0 == f.list(new FilenameFilter() {
322 public boolean accept(File fdir, String name) {
323 File file = new File(dir_mipmaps + name);
324 if (file.isHidden() || '.' == name.charAt(0)) return false;
325 return true;
327 }).length) {
328 try { f.delete(); } catch (Exception e) { Utils.log("Could not remove empty trakem2.mipmaps directory."); }
331 // remove crash detector
332 File f = new File(dir_mipmaps + ".open.t2");
333 try {
334 if (!f.delete()) {
335 Utils.log2("WARNING: could not delete crash detector file .open.t2 from trakem2.mipmaps folder at " + dir_mipmaps);
337 } catch (Exception e) {
338 Utils.log2("WARNING: crash detector file trakem.mipmaps/.open.t2 may NOT have been deleted.");
339 IJError.print(e);
343 /** Get the next unique id, not shared by any other object within the same project. */
344 public long getNextId() {
345 long nid = -1;
346 synchronized (db_lock) {
347 lock();
348 nid = ++max_id;
349 unlock();
351 return nid;
354 /** Loaded in full from XML file */
355 public double[][][] fetchBezierArrays(long id) {
356 return null;
359 /** Loaded in full from XML file */
360 public ArrayList fetchPipePoints(long id) {
361 return null;
364 /** Loaded in full from XML file */
365 public ArrayList fetchBallPoints(long id) {
366 return null;
369 /** Loaded in full from XML file */
370 public Area fetchArea(long area_list_id, long layer_id) {
371 return null;
374 /* Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImagePlust.getProcessor().
375 * or just use the Patch.getImageProcessor() method which does it for you. */
376 public ImagePlus fetchImagePlus(final Patch p) {
377 return (ImagePlus)fetchImage(p, Layer.IMAGEPLUS);
380 /** Fetch the ImageProcessor in a synchronized manner, so that there are no conflicts in retrieving the ImageProcessor for a specific stack slice, for example.
381 * Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImageProcessor,
382 * or just use the Patch.getImageProcessor() method which does it for you. */
383 public ImageProcessor fetchImageProcessor(final Patch p) {
384 return (ImageProcessor)fetchImage(p, Layer.IMAGEPROCESSOR);
387 /** So far accepts Layer.IMAGEPLUS and Layer.IMAGEPROCESSOR as format. */
388 public Object fetchImage(final Patch p, final int format) {
389 ImagePlus imp = null;
390 ImageProcessor ip = null;
391 String slice = null;
392 String path = null;
393 long n_bytes = 0;
394 PatchLoadingLock plock = null;
395 synchronized (db_lock) {
396 lock();
397 imp = imps.get(p.getId());
398 try {
399 path = getAbsolutePath(p);
400 int i_sl = -1;
401 if (null != path) i_sl = path.lastIndexOf("-----#slice=");
402 if (-1 != i_sl) {
403 // activate proper slice
404 if (null != imp) {
405 // check that the stack is large enough (user may have changed it)
406 final int ia = Integer.parseInt(path.substring(i_sl + 12));
407 if (ia <= imp.getNSlices()) {
408 if (null == imp.getStack() || null == imp.getStack().getPixels(ia)) {
409 // reload (happens when closing a stack that was opened before importing it, and then trying to paint, for example)
410 imps.remove(p.getId());
411 imp = null;
412 } else {
413 imp.setSlice(ia);
414 switch (format) {
415 case Layer.IMAGEPROCESSOR:
416 ip = imp.getStack().getProcessor(ia);
417 unlock();
418 return ip;
419 case Layer.IMAGEPLUS:
420 unlock();
421 return imp;
422 default:
423 Utils.log("FSLoader.fetchImage: Unknown format " + format);
424 return null;
427 } else {
428 unlock();
429 return null; // beyond bonds!
433 // for non-stack images
434 if (null != imp) {
435 unlock();
436 switch (format) {
437 case Layer.IMAGEPROCESSOR:
438 return imp.getProcessor();
439 case Layer.IMAGEPLUS:
440 return imp;
441 default:
442 Utils.log("FSLoader.fetchImage: Unknown format " + format);
443 return null;
446 if (-1 != i_sl) {
447 slice = path.substring(i_sl);
448 // set path proper
449 path = path.substring(0, i_sl);
452 releaseMemory(); // ensure there is a minimum % of free memory
453 plock = getOrMakePatchLoadingLock(p, 0);
454 } catch (Exception e) {
455 IJError.print(e);
456 return null;
457 } finally {
458 unlock();
463 synchronized (plock) {
464 plock.lock();
466 imp = imps.get(p.getId());
467 if (null != imp) {
468 // was loaded by a different thread
469 plock.unlock();
470 switch (format) {
471 case Layer.IMAGEPROCESSOR:
472 return imp.getProcessor();
473 case Layer.IMAGEPLUS:
474 return imp;
475 default:
476 Utils.log("FSLoader.fetchImage: Unknown format " + format);
477 return null;
481 // going to load:
484 // reserve memory:
485 synchronized (db_lock) {
486 lock();
487 n_bytes = estimateImageFileSize(p, 0);
488 max_memory -= n_bytes;
489 unlock();
492 releaseToFit(n_bytes);
493 imp = openImage(path);
495 preProcess(imp);
497 synchronized (db_lock) {
498 try {
499 lock();
500 max_memory += n_bytes;
502 if (null == imp) {
503 if (!hs_unloadable.contains(p)) {
504 Utils.log("FSLoader.fetchImagePlus: no image exists for patch " + p + " at path " + path);
505 hs_unloadable.add(p);
507 removePatchLoadingLock(plock);
508 unlock();
509 plock.unlock();
510 return null;
512 // update all clients of the stack, if any
513 if (null != slice) {
514 String rel_path = getPath(p); // possibly relative
515 final int r_isl = rel_path.lastIndexOf("-----#slice");
516 if (-1 != r_isl) rel_path = rel_path.substring(0, r_isl); // should always happen
517 for (Iterator<Map.Entry<Long,String>> it = ht_paths.entrySet().iterator(); it.hasNext(); ) {
518 final Map.Entry<Long,String> entry = it.next();
519 final String str = entry.getValue(); // this is like calling getPath(p)
520 //Utils.log2("processing " + str);
521 if (0 != str.indexOf(rel_path)) {
522 //Utils.log2("SKIP str is: " + str + "\t but path is: " + rel_path);
523 continue; // get only those whose path is identical, of course!
525 final int isl = str.lastIndexOf("-----#slice=");
526 if (-1 != isl) {
527 //int i_slice = Integer.parseInt(str.substring(isl + 12));
528 final long lid = entry.getKey();
529 imps.put(lid, imp);
532 // set proper active slice
533 final int ia = Integer.parseInt(slice.substring(12));
534 imp.setSlice(ia);
535 if (Layer.IMAGEPROCESSOR == format) ip = imp.getStack().getProcessor(ia); // otherwise creates one new for nothing
536 } else {
537 // for non-stack images
538 // OBSOLETE and wrong //p.putMinAndMax(imp); // non-destructive contrast: min and max -- WRONG, it's destructive for ColorProcessor and ByteProcessor!
539 // puts the Patch min and max values into the ImagePlus processor.
540 imps.put(p.getId(), imp);
541 if (Layer.IMAGEPROCESSOR == format) ip = imp.getProcessor();
543 // imp is cached, so:
544 removePatchLoadingLock(plock);
546 } catch (Exception e) {
547 IJError.print(e);
549 unlock();
550 plock.unlock();
551 switch (format) {
552 case Layer.IMAGEPROCESSOR:
553 return ip; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
554 case Layer.IMAGEPLUS:
555 return imp;
556 default:
557 Utils.log("FSLoader.fetchImage: Unknown format " + format);
558 return null;
565 /** Returns the alpha mask image from a file, or null if none stored. */
566 @Override
567 public ByteProcessor fetchImageMask(final Patch p) {
568 // Else, see if there is a file for the Patch:
569 final String path = getAlphaPath(p);
570 if (null == path) return null;
571 // Open the mask image, which should be a compressed float tif.
572 final ImagePlus imp = opener.openImage(path);
573 if (null == imp) {
574 Utils.log2("No mask found or could not open mask image for patch " + p + " from " + path);
575 return null;
577 return (ByteProcessor)imp.getProcessor().convertToByte(false);
580 @Override
581 public String getAlphaPath(final Patch p) {
582 final String filename = getInternalFileName(p);
583 if (null == filename) {
584 Utils.log2("null filepath!");
585 return null;
587 final String dir = getMasksFolder();
588 return new StringBuffer(dir).append(filename).append('.').append(p.getId()).append(".zip").toString();
591 @Override
592 public void storeAlphaMask(final Patch p, final ByteProcessor fp) {
593 // would fail if user deletes the trakem2.masks/ folder from the storage folder after having set dir_masks. But that is his problem.
594 new FileSaver(new ImagePlus("mask", fp)).saveAsZip(getAlphaPath(p));
597 public final String getMasksFolder() {
598 if (null == dir_masks) createMasksFolder();
599 return dir_masks;
602 synchronized private final void createMasksFolder() {
603 if (null == dir_masks) dir_masks = getStorageFolder() + "trakem2.masks/";
604 final File f = new File(dir_masks);
605 if (f.exists() && f.isDirectory()) return;
606 try {
607 f.mkdir();
608 } catch (Exception e) {
609 IJError.print(e);
613 /** Remove the file containing the given Patch's alpha mask. */
614 public final boolean removeAlphaMask(final Patch p) {
615 try {
616 File f = new File(getAlphaPath(p));
617 if (f.exists()) {
618 return f.delete();
620 return true;
621 } catch (Exception e) {
622 IJError.print(e);
624 return false;
627 /** Loaded in full from XML file */
628 public Object[] fetchLabel(DLabel label) {
629 return null;
632 /** Loads and returns the original image, which is not cached, or returns null if it's not different than the working image. */
633 synchronized public ImagePlus fetchOriginal(final Patch patch) {
634 String original_path = patch.getOriginalPath();
635 if (null == original_path) return null;
636 // else, reserve memory and open it:
637 long n_bytes = estimateImageFileSize(patch, 0);
638 // reserve memory:
639 synchronized (db_lock) {
640 lock();
641 max_memory -= n_bytes;
642 unlock();
644 try {
645 return openImage(original_path);
646 } catch (Throwable t) {
647 IJError.print(t);
648 } finally {
649 synchronized (db_lock) {
650 lock();
651 max_memory += n_bytes;
652 unlock();
655 return null;
658 public void prepare(Layer layer) {
659 //Utils.log2("FSLoader.prepare(Layer): not implemented.");
660 super.prepare(layer);
663 /* GENERIC, from DBObject calls. Records the id of the object in the HashMap ht_dbo.
664 * Always returns true. Does not check if another object has the same id.
666 public boolean addToDatabase(final DBObject ob) {
667 synchronized (db_lock) {
668 lock();
669 setChanged(true);
670 final long id = ob.getId();
671 if (id > max_id) {
672 max_id = id;
674 unlock();
676 return true;
679 public boolean updateInDatabase(final DBObject ob, final String key) {
680 setChanged(true);
681 if (ob.getClass() == Patch.class) {
682 Patch p = (Patch)ob;
683 if (key.equals("tiff_working")) return null != setImageFile(p, fetchImagePlus(p));
685 return true;
688 public boolean removeFromDatabase(final DBObject ob) {
689 synchronized (db_lock) {
690 lock();
691 setChanged(true);
692 // remove from the hashtable
693 final long loid = ob.getId();
694 Utils.log2("removing " + Project.getName(ob.getClass()) + " " + ob);
695 if (ob.getClass() == Patch.class) {
696 // STRATEGY change: images are not owned by the FSLoader.
697 Patch p = (Patch)ob;
698 if (!ob.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p);
699 ht_paths.remove(p.getId()); // after removeMipMaps !
700 mawts.removeAndFlush(loid);
701 final ImagePlus imp = imps.remove(loid);
702 if (null != imp) {
703 if (imp.getStackSize() > 1) {
704 if (null == imp.getProcessor()) {}
705 else if (null == imp.getProcessor().getPixels()) {}
706 else Loader.flush(imp); // only once
707 } else {
708 Loader.flush(imp);
711 cannot_regenerate.remove(p);
712 unlock();
713 flushMipMaps(p.getId()); // locks on its own
714 touched_mipmaps.remove(p);
715 return true;
717 unlock();
719 return true;
722 /** Returns the absolute path to a file that contains the given ImagePlus image - which may be the path as described in the ImagePlus FileInfo object itself, or a totally new file.
723 * If the Patch p current image path is different than its original image path, then the file is overwritten if it exists already.
725 public String setImageFile(final Patch p, final ImagePlus imp) {
726 if (null == imp) return null;
727 try {
728 String path = getAbsolutePath(p);
729 String slice = null;
731 // path can be null if the image is pasted, or from a copy, or totally new
732 if (null != path) {
733 int i_sl = path.lastIndexOf("-----#slice=");
734 if (-1 != i_sl) {
735 slice = path.substring(i_sl);
736 path = path.substring(0, i_sl);
738 } else {
739 // no path, inspect image FileInfo's path if the image has no changes
740 if (!imp.changes) {
741 final FileInfo fi = imp.getOriginalFileInfo();
742 if (null != fi && null != fi.directory && null != fi.fileName) {
743 final String fipath = fi.directory.replace('\\', '/') + "/" + fi.fileName;
744 if (new File(fipath).exists()) {
745 // no need to save a new image, it exists and has no changes
746 updatePaths(p, fipath, null != slice);
747 cacheAll(p, imp);
748 Utils.log2("Reusing image file: path exists for fileinfo at " + fipath);
749 return fipath;
754 if (null != path) {
755 final String starting_path = path;
756 // Save as a separate image in a new path within the storage folder
758 String filename = path.substring(path.lastIndexOf('/') +1);
760 //Utils.log2("filename 1: " + filename);
762 // remove .tif extension if there
763 if (filename.endsWith(".tif")) filename = filename.substring(0, filename.length() -3); // keep the dot
765 //Utils.log2("filename 2: " + filename);
767 // check if file ends with a tag of form ".id1234." where 1234 is p.getId()
768 final String tag = ".id" + p.getId() + ".";
769 if (!filename.endsWith(tag)) filename += tag.substring(1); // without the starting dot, since it has one already
770 // reappend extension
771 filename += "tif";
773 //Utils.log2("filename 3: " + filename);
775 path = getImageStorageFolder() + filename;
777 if (path.equals(p.getOriginalPath())) {
778 // Houston, we have a problem: a user reused a non-original image
779 File file = null;
780 int i = 1;
781 final int itag = path.lastIndexOf(tag);
782 do {
783 path = path.substring(0, itag) + "." + i + tag + "tif";
784 i++;
785 file = new File(path);
786 } while (file.exists());
789 //Utils.log2("path to use: " + path);
791 final String path2 = super.exportImage(p, imp, path, true);
793 //Utils.log2("path exported to: " + path2);
795 // update paths' hashtable
796 if (null != path2) {
797 updatePaths(p, path2, null != slice);
798 cacheAll(p, imp);
799 hs_unloadable.remove(p);
800 return path2;
801 } else {
802 Utils.log("WARNING could not save image at " + path);
803 // undo
804 updatePaths(p, starting_path, null != slice);
805 return null;
808 } catch (Exception e) {
809 IJError.print(e);
811 return null;
815 * TODO
816 * Never used. Was this planned to be what we do no with DBObject.getUniqueId()?
818 private final String makeFileTitle(final Patch p) {
819 String title = p.getTitle();
820 if (null == title) return "image-" + p.getId();
821 title = asSafePath(title);
822 if (0 == title.length()) return "image-" + p.getId();
823 return title;
826 /** Associate patch with imp, and all slices as well if any. */
827 private void cacheAll(final Patch p, final ImagePlus imp) {
828 if (p.isStack()) {
829 for (Patch pa : p.getStackPatches()) {
830 cache(pa, imp);
832 } else {
833 cache(p, imp);
837 /** For the Patch and for any associated slices if the patch is part of a stack. */
838 private void updatePaths(final Patch patch, final String path, final boolean is_stack) {
839 synchronized (db_lock) {
840 lock();
841 try {
842 // ensure the old path is cached in the Patch, to get set as the original if there is no original.
843 if (is_stack) {
844 for (Patch p : patch.getStackPatches()) {
845 long pid = p.getId();
846 String str = ht_paths.get(pid);
847 int isl = str.lastIndexOf("-----#slice=");
848 updatePatchPath(p, path + str.substring(isl));
850 } else {
851 Utils.log2("path to set: " + path);
852 Utils.log2("path before: " + ht_paths.get(patch.getId()));
853 updatePatchPath(patch, path);
854 Utils.log2("path after: " + ht_paths.get(patch.getId()));
856 } catch (Throwable e) {
857 IJError.print(e);
858 } finally {
859 unlock();
864 /** With slice info appended at the end; only if it exists, otherwise null. */
865 public String getAbsolutePath(final Patch patch) {
866 String abs_path = patch.getCurrentPath();
867 if (null != abs_path) return abs_path;
868 // else, compute, set and return it:
869 String path = ht_paths.get(patch.getId());
870 if (null == path) return null;
871 // substract slice info if there
872 int i_sl = path.lastIndexOf("-----#slice=");
873 String slice = null;
874 if (-1 != i_sl) {
875 slice = path.substring(i_sl);
876 path = path.substring(0, i_sl);
878 if (isRelativePath(path)) {
879 // path is relative: preprend the parent folder of the xml file
880 path = getParentFolder() + path;
881 if (!isURL(path) && !new File(path).exists()) {
882 Utils.log("Path for patch " + patch + " does not exist: " + path);
883 return null;
885 // else assume that it exists
887 // reappend slice info if existent
888 if (null != slice) path += slice;
889 // set it
890 patch.cacheCurrentPath(path);
891 return path;
894 public static final boolean isURL(final String path) {
895 return null != path && 0 == path.indexOf("http://");
898 public static final boolean isRelativePath(final String path) {
899 if (((!IJ.isWindows() && 0 != path.indexOf('/')) || (IJ.isWindows() && 1 != path.indexOf(":/"))) && 0 != path.indexOf("http://") && 0 != path.indexOf("//")) { // "//" is for Samba networks (since the \\ has been converted to // before)
900 return true;
902 return false;
905 /** All backslashes are converted to slashes to avoid havoc in MSWindows. */
906 public void addedPatchFrom(String path, final Patch patch) {
907 if (null == path) {
908 Utils.log("Null path for patch: " + patch);
909 return;
911 updatePatchPath(patch, path);
914 /** This method has the exclusivity in calling ht_paths.put, because it ensures the path won't have escape characters. */
915 private final void updatePatchPath(final Patch patch, String path) { // reversed order in purpose, relative to addedPatchFrom
916 // avoid W1nd0ws nightmares
917 path = path.replace('\\', '/'); // replacing with chars, in place
918 // remove double slashes that a user may have slipped in
919 final int start = isURL(path) ? 6 : (IJ.isWindows() ? 3 : 1);
920 while (-1 != path.indexOf("//", start)) {
921 // avoid the potential C:// of windows and the starting // of a samba network
922 path = path.substring(0, start) + path.substring(start).replace("//", "/");
924 // cache path as absolute
925 patch.cacheCurrentPath(isRelativePath(path) ? getParentFolder() + path : path);
926 // if path is absolute, try to make it relative
927 path = makeRelativePath(path);
928 // store
929 ht_paths.put(patch.getId(), path);
930 //Utils.log2("Updated patch path " + ht_paths.get(patch.getId()) + " for patch " + patch);
933 /** Takes a String and returns a copy with the following conversions: / to -, space to _, and \ to -. */
934 public String asSafePath(final String name) {
935 return name.trim().replace('/', '-').replace(' ', '_').replace('\\','-');
938 /** Overwrites the XML file. If some images do not exist in the file system, a directory with the same name of the XML file plus an "_images" tag appended will be created and images saved there. */
939 public String save(final Project project) {
940 String result = null;
941 if (null == project_file_path) {
942 String xml_path = super.saveAs(project, null, false);
943 if (null == xml_path) return null;
944 else {
945 this.project_file_path = xml_path;
946 ControlWindow.updateTitle(project);
947 result = this.project_file_path;
949 } else {
950 File fxml = new File(project_file_path);
951 result = super.export(project, fxml, false);
953 if (null != result) {
954 Utils.logAll(Utils.now() + " Saved " + project);
955 touched_mipmaps.clear();
957 return result;
960 public String saveAs(Project project) {
961 String path = super.saveAs(project, null, false);
962 if (null != path) {
963 // update the xml path to point to the new one
964 this.project_file_path = path;
965 Utils.log2("After saveAs, new xml path is: " + path);
967 ControlWindow.updateTitle(project);
968 return path;
971 /** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
972 public String saveAs(final String path, final boolean overwrite) {
973 if (null == path) {
974 Utils.log("Cannot save on null path.");
975 return null;
977 String path2 = path;
978 if (!path2.endsWith(".xml")) path2 += ".xml";
979 File fxml = new File(path2);
980 if (!fxml.canWrite()) {
981 // write to storage folder instead
982 String path3 = path2;
983 path2 = getStorageFolder() + fxml.getName();
984 Utils.logAll("WARNING can't write to " + path3 + "\n --> will write instead to " + path2);
985 fxml = new File(path2);
987 if (!overwrite) {
988 int i = 1;
989 while (fxml.exists()) {
990 String parent = fxml.getParent().replace('\\','/');
991 if (!parent.endsWith("/")) parent += "/";
992 String name = fxml.getName();
993 name = name.substring(0, name.length() - 4);
994 path2 = parent + name + "-" + i + ".xml";
995 fxml = new File(path2);
996 i++;
999 Project project = Project.findProject(this);
1000 path2 = super.saveAs(project, path2, false);
1001 if (null != path2) {
1002 project_file_path = path2;
1003 Utils.logAll("After saveAs, new xml path is: " + path2);
1004 ControlWindow.updateTitle(project);
1005 touched_mipmaps.clear();
1007 return path2;
1010 /** Returns the stored path for the given Patch image, which may be relative and may contain slice information appended.*/
1011 public String getPath(final Patch patch) {
1012 return ht_paths.get(patch.getId());
1015 /** Takes the given path and tries to makes it relative to this instance's project_file_path, if possible. Otherwise returns the argument as is. */
1016 private String makeRelativePath(String path) {
1017 if (null == project_file_path) {
1018 //unsaved project
1019 return path;
1021 if (null == path) {
1022 return null;
1024 // fix W1nd0ws paths
1025 path = path.replace('\\', '/'); // char-based, no parsing problems
1026 // remove slice tag
1027 String slice = null;
1028 int isl = path.lastIndexOf("-----#slice");
1029 if (-1 != isl) {
1030 slice = path.substring(isl);
1031 path = path.substring(0, isl);
1034 if (isRelativePath(path)) {
1035 // already relative
1036 if (-1 != isl) path += slice;
1037 return path;
1039 // the long and verbose way, to be cross-platform. Should work with URLs just the same.
1040 File xf = new File(project_file_path);
1041 File fpath = new File(path);
1042 if (fpath.getParentFile().equals(xf.getParentFile())) {
1043 path = path.substring(xf.getParent().length());
1044 // remove prepended file separator, if any, to label the path as relative
1045 if (0 == path.indexOf('/')) path = path.substring(1);
1046 } else if (fpath.equals(xf.getParentFile())) {
1047 return "";
1049 if (-1 != isl) path += slice;
1050 //Utils.log("made relative path: " + path);
1051 return path;
1054 /** Adds a "Save" and "Save as" menu items. */
1055 public void setupMenuItems(final JMenu menu, final Project project) {
1056 ActionListener listener = new ActionListener() {
1057 public void actionPerformed(ActionEvent ae) {
1058 String command = ae.getActionCommand();
1059 if (command.equals("Save")) {
1060 save(project);
1061 } else if (command.equals("Save as...")) {
1062 saveAs(project);
1066 JMenuItem item;
1067 item = new JMenuItem("Save"); item.addActionListener(listener); menu.add(item);
1068 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true));
1069 item = new JMenuItem("Save as..."); item.addActionListener(listener); menu.add(item);
1072 /** Returns the last Patch. */
1073 protected Patch importStackAsPatches(final Project project, final Layer first_layer, final int x, final int y, final ImagePlus imp_stack, final boolean as_copy, final String filepath) {
1074 Utils.log2("FSLoader.importStackAsPatches filepath=" + filepath);
1075 String target_dir = null;
1076 if (as_copy) {
1077 DirectoryChooser dc = new DirectoryChooser("Folder to save images");
1078 target_dir = dc.getDirectory();
1079 if (null == target_dir) return null; // user canceled dialog
1080 if (target_dir.length() -1 != target_dir.lastIndexOf('/')) {
1081 target_dir += "/";
1085 final boolean virtual = imp_stack.getStack().isVirtual();
1087 int pos_x = Integer.MAX_VALUE != x ? x : (int)first_layer.getLayerWidth()/2 - imp_stack.getWidth()/2;
1088 int pos_y = Integer.MAX_VALUE != y ? y : (int)first_layer.getLayerHeight()/2 - imp_stack.getHeight()/2;
1089 final double thickness = first_layer.getThickness();
1090 final String title = Utils.removeExtension(imp_stack.getTitle()).replace(' ', '_');
1091 Utils.showProgress(0);
1092 Patch previous_patch = null;
1093 final int n = imp_stack.getStackSize();
1094 for (int i=1; i<=n; i++) {
1095 Layer layer = first_layer;
1096 double z = first_layer.getZ() + (i-1) * thickness;
1097 if (i > 1) layer = first_layer.getParent().getLayer(z, thickness, true); // will create new layer if not found
1098 if (null == layer) {
1099 Utils.log("Display.importStack: could not create new layers.");
1100 return null;
1102 String patch_path = null;
1104 ImagePlus imp_patch_i = null;
1105 if (virtual) { // because we love inefficiency, every time all this is done again
1106 VirtualStack vs = (VirtualStack)imp_stack.getStack();
1107 String vs_dir = vs.getDirectory().replace('\\', '/');
1108 if (!vs_dir.endsWith("/")) vs_dir += "/";
1109 String iname = vs.getFileName(i);
1110 patch_path = vs_dir + iname;
1111 releaseMemory();
1112 Utils.log2(i + " : " + patch_path);
1113 imp_patch_i = openImage(patch_path);
1114 } else {
1115 ImageProcessor ip = imp_stack.getStack().getProcessor(i);
1116 if (as_copy) ip = ip.duplicate();
1117 imp_patch_i = new ImagePlus(title + "__slice=" + i, ip);
1119 preProcess(imp_patch_i);
1121 String label = imp_stack.getStack().getSliceLabel(i);
1122 if (null == label) label = "";
1123 Patch patch = null;
1124 if (as_copy) {
1125 patch_path = target_dir + imp_patch_i.getTitle() + ".zip";
1126 ini.trakem2.io.ImageSaver.saveAsZip(imp_patch_i, patch_path);
1127 patch = new Patch(project, label + " " + title + " " + i, pos_x, pos_y, imp_patch_i);
1128 } else if (virtual) {
1129 patch = new Patch(project, label, pos_x, pos_y, imp_patch_i);
1130 } else {
1131 patch_path = filepath + "-----#slice=" + i;
1132 //Utils.log2("path is "+ patch_path);
1133 final AffineTransform atp = new AffineTransform();
1134 atp.translate(pos_x, pos_y);
1135 patch = new Patch(project, getNextId(), label + " " + title + " " + i, imp_stack.getWidth(), imp_stack.getHeight(), imp_stack.getType(), false, imp_stack.getProcessor().getMin(), imp_stack.getProcessor().getMax(), atp);
1136 patch.addToDatabase();
1137 //Utils.log2("type is " + imp_stack.getType());
1139 Utils.log2("B: " + i + " : " + patch_path);
1140 addedPatchFrom(patch_path, patch);
1141 if (!as_copy) {
1142 if (virtual) cache(patch, imp_patch_i); // each slice separately
1143 else cache(patch, imp_stack); // uses the entire stack, shared among all Patch instances
1145 if (isMipMapsEnabled()) generateMipMaps(patch);
1146 if (null != previous_patch) patch.link(previous_patch);
1147 layer.add(patch);
1148 previous_patch = patch;
1149 Utils.showProgress(i * (1.0 / n));
1151 Utils.showProgress(1.0);
1153 // update calibration
1154 // TODO
1156 // return the last patch
1157 return previous_patch;
1160 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1161 public void parseXMLOptions(final HashMap ht_attributes) {
1162 Object ob = ht_attributes.remove("preprocessor");
1163 if (null != ob) {
1164 setPreprocessor((String)ob);
1166 // Adding some logic to support old projects which lack a storage folder and a mipmaps folder
1167 // and also to prevent errors such as those created when manualy tinkering with the XML file
1168 // or renaming directories, etc.
1169 ob = ht_attributes.remove("storage_folder");
1170 if (null != ob) {
1171 String sf = ((String)ob).replace('\\', '/');
1172 if (isRelativePath(sf)) {
1173 sf = getParentFolder() + sf;
1175 if (isURL(sf)) {
1176 // can't be an URL
1177 Utils.log2("Can't have an URL as the path of a storage folder.");
1178 } else {
1179 File f = new File(sf);
1180 if (f.exists() && f.isDirectory()) {
1181 this.dir_storage = sf;
1182 } else {
1183 Utils.log2("storage_folder was not found or is invalid: " + ob);
1187 if (null == this.dir_storage) {
1188 // select the directory where the xml file lives.
1189 this.dir_storage = getParentFolder();
1190 if (null == this.dir_storage || isURL(this.dir_storage)) this.dir_storage = null;
1191 if (null == this.dir_storage && ControlWindow.isGUIEnabled()) {
1192 Utils.log2("Asking user for a storage folder in a dialog."); // tip for headless runners whose program gets "stuck"
1193 DirectoryChooser dc = new DirectoryChooser("REQUIRED: select a storage folder");
1194 this.dir_storage = dc.getDirectory();
1196 if (null == this.dir_storage) {
1197 IJ.showMessage("TrakEM2 requires a storage folder.\nTemporarily your home directory will be used.");
1198 this.dir_storage = System.getProperty("user.home").replace('\\', '/');
1201 // fix
1202 if (null != this.dir_storage && !this.dir_storage.endsWith("/")) this.dir_storage += "/";
1203 Utils.log2("storage folder is " + this.dir_storage);
1205 ob = ht_attributes.remove("mipmaps_folder");
1206 if (null != ob) {
1207 String mf = ((String)ob).replace('\\', '/');
1208 if (isRelativePath(mf)) {
1209 mf = getParentFolder() + mf;
1211 if (isURL(mf)) {
1212 this.dir_mipmaps = mf;
1213 // TODO must disable input somehow, so that images are not edited.
1214 } else {
1215 File f = new File(mf);
1216 if (f.exists() && f.isDirectory()) {
1217 this.dir_mipmaps = mf;
1218 } else {
1219 Utils.log2("mipmaps_folder was not found or is invalid: " + ob);
1223 if (null == this.dir_mipmaps) {
1224 // create a new one inside the dir_storage, which can't be null
1225 createMipMapsDir(dir_storage);
1226 if (null != this.dir_mipmaps && ControlWindow.isGUIEnabled() && null != IJ.getInstance()) {
1227 askAndExecMipmapRegeneration(null);
1230 // fix
1231 if (null != this.dir_mipmaps && !this.dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
1232 Utils.log2("mipmaps folder is " + this.dir_mipmaps);
1235 private void askAndExecMipmapRegeneration(final String msg) {
1236 Utils.log2("Asking user Yes/No to generate mipmaps on the background."); // tip for headless runners whose program gets "stuck"
1237 YesNoDialog yn = new YesNoDialog(IJ.getInstance(), "Generate mipmaps", (null != msg ? msg + "\n" : "") + "Generate mipmaps in the background for all images?");
1238 if (yn.yesPressed()) {
1239 final Loader lo = this;
1240 new Thread() {
1241 public void run() {
1242 try {
1243 // wait while parsing the rest of the XML file
1244 while (!v_loaders.contains(lo)) {
1245 Thread.sleep(1000);
1247 Project pj = Project.findProject(lo);
1248 lo.generateMipMaps(pj.getRootLayerSet().getDisplayables(Patch.class));
1249 } catch (Exception e) {}
1251 }.start();
1255 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1256 public void insertXMLOptions(StringBuffer sb_body, String indent) {
1257 if (null != preprocessor) sb_body.append(indent).append("preprocessor=\"").append(preprocessor).append("\"\n");
1258 if (null != dir_mipmaps) sb_body.append(indent).append("mipmaps_folder=\"").append(makeRelativePath(dir_mipmaps)).append("\"\n");
1259 if (null != dir_storage) sb_body.append(indent).append("storage_folder=\"").append(makeRelativePath(dir_storage)).append("\"\n");
1262 /** Return the path to the folder containing the project XML file. */
1263 private final String getParentFolder() {
1264 return this.project_file_path.substring(0, this.project_file_path.lastIndexOf('/')+1);
1267 /* ************** MIPMAPS **********************/
1269 /** Returns the path to the directory hosting the file image pyramids. */
1270 public String getMipMapsFolder() {
1271 return dir_mipmaps;
1276 static private IndexColorModel thresh_cm = null;
1278 static private final IndexColorModel getThresholdLUT() {
1279 if (null == thresh_cm) {
1280 // An array of all black pixels (value 0) except at 255, which is white (value 255).
1281 final byte[] c = new byte[256];
1282 c[255] = (byte)255;
1283 thresh_cm = new IndexColorModel(8, 256, c, c, c);
1285 return thresh_cm;
1289 /** Returns the array of pixels, whose type depends on the bi.getType(); for example, for a BufferedImage.TYPE_BYTE_INDEXED, returns a byte[]. */
1290 static public final Object grabPixels(final BufferedImage bi) {
1291 final PixelGrabber pg = new PixelGrabber(bi, 0, 0, bi.getWidth(), bi.getHeight(), false);
1292 try {
1293 pg.grabPixels();
1294 return pg.getPixels();
1295 } catch (InterruptedException e) {
1296 IJError.print(e);
1298 return null;
1301 private final BufferedImage createCroppedAlpha(final BufferedImage alpha, final BufferedImage outside) {
1302 if (null == outside) return alpha;
1304 final int width = outside.getWidth();
1305 final int height = outside.getHeight();
1307 // Create an outside image, thresholded: only pixels of 255 remain as 255, the rest is set to 0.
1308 /* // DOESN'T work: creates a mask with "black" as 254 (???), and white 255 (correct).
1309 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, getThresholdLUT());
1310 thresholded.createGraphics().drawImage(outside, 0, 0, null);
1313 // So, instead: grab the pixels, fix them manually
1314 // The cast to byte[] works because "outside" and "alpha" are TYPE_BYTE_INDEXED.
1315 final byte[] o = (byte[])grabPixels(outside);
1316 if (null == o) return null;
1317 final byte[] a = null == alpha ? o : (byte[])grabPixels(alpha);
1319 // Set each non-255 pixel in outside to 0 in alpha:
1320 for (int i=0; i<o.length; i++) {
1321 if ( (o[i]&0xff) < 255) a[i] = 0;
1324 // Put the pixels back into an image:
1325 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
1326 thresholded.getRaster().setDataElements(0, 0, width, height, a);
1328 return thresholded;
1331 static public final BufferedImage convertToBufferedImage(final ByteProcessor bp) {
1332 bp.setMinAndMax(0, 255);
1333 final Image img = bp.createImage();
1334 if (img instanceof BufferedImage) return (BufferedImage)img;
1335 //else:
1336 final BufferedImage bi = new BufferedImage(bp.getWidth(), bp.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
1337 bi.createGraphics().drawImage(bi, 0, 0, null);
1338 return bi;
1341 /** Scale a BufferedImage.TYPE_BYTE_INDEXED into another of the same type but dimensions target_width,target_height. */
1342 static private final BufferedImage scaleAndFlush(final Image img, final int target_width, final int target_height, final boolean area_averaging, final Object interpolation_hint) {
1343 final BufferedImage bi = new BufferedImage(target_width, target_height, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
1344 if (area_averaging) {
1345 bi.createGraphics().drawImage(img.getScaledInstance(target_width, target_height, Image.SCALE_AREA_AVERAGING), 0, 0, null);
1346 } else {
1347 final Graphics2D g = bi.createGraphics();
1348 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation_hint);
1349 g.drawImage(img, 0, 0, target_width, target_height, null); // draws it scaled to target area w*h
1351 // Release native resources
1352 img.flush();
1354 return bi;
1357 /** Image to BufferedImage. Can be used for hardware-accelerated resizing, since the whole awt is painted to a target w,h area in the returned new BufferedImage. Does not accept LUT images: only ARGB or GRAY. */
1358 private final BufferedImage[] IToBI(final Image awt, final int w, final int h, final Object interpolation_hint, final BufferedImage alpha, final BufferedImage outside) {
1359 BufferedImage bi;
1360 final boolean area_averaging = interpolation_hint.getClass() == Integer.class && Loader.AREA_AVERAGING == ((Integer)interpolation_hint).intValue();
1361 final boolean must_scale = (w != awt.getWidth(null) || h != awt.getHeight(null));
1363 if (null != alpha || null != outside) bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1364 else bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1365 final Graphics2D g = bi.createGraphics();
1366 if (area_averaging) {
1367 final Image img = awt.getScaledInstance(w, h, Image.SCALE_AREA_AVERAGING); // Creates ALWAYS an RGB image, so must repaint back to a single-channel image, avoiding unnecessary blow up of memory.
1368 g.drawImage(img, 0, 0, null);
1369 } else {
1370 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation_hint);
1371 g.drawImage(awt, 0, 0, w, h, null); // draws it scaled
1373 BufferedImage ba = alpha;
1374 BufferedImage bo = outside;
1375 if (null != alpha && must_scale) {
1376 ba = scaleAndFlush(alpha, w, h, area_averaging, interpolation_hint);
1378 if (null != outside && must_scale) {
1379 bo = scaleAndFlush(outside, w, h, area_averaging, interpolation_hint);
1382 BufferedImage the_alpha = ba;
1383 if (null != alpha) {
1384 if (null != outside) {
1385 the_alpha = createCroppedAlpha(ba, bo);
1387 } else if (null != outside) {
1388 the_alpha = createCroppedAlpha(null, bo);
1390 if (null != the_alpha) {
1391 bi.getAlphaRaster().setRect(the_alpha.getRaster());
1392 //bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])new ImagePlus("", the_alpha).getProcessor().convertToFloat().getPixels());
1393 the_alpha.flush();
1396 //Utils.log2("bi is: " + bi.getType() + " BufferedImage.TYPE_INT_ARGB=" + BufferedImage.TYPE_INT_ARGB);
1399 FloatProcessor fp_alpha = null;
1400 fp_alpha = (FloatProcessor) new ByteProcessor(ba).convertToFloat();
1401 // Set all non-white pixels to zero (eliminate shadowy border caused by interpolation)
1402 final float[] pix = (float[])fp_alpha.getPixels();
1403 for (int i=0; i<pix.length; i++)
1404 if (Math.abs(pix[i] - 255) > 0.001f) pix[i] = 0;
1405 bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])fp_alpha.getPixels());
1408 return new BufferedImage[]{bi, ba, bo};
1411 private final Object getHint(final int mode) {
1412 switch (mode) {
1413 case Loader.BICUBIC:
1414 return RenderingHints.VALUE_INTERPOLATION_BICUBIC;
1415 case Loader.BILINEAR:
1416 return RenderingHints.VALUE_INTERPOLATION_BILINEAR;
1417 case Loader.AREA_AVERAGING:
1418 return new Integer(mode);
1419 case Loader.NEAREST_NEIGHBOR:
1420 default:
1421 return RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
1425 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1426 static final private byte[] gaussianBlurResizeInHalf(final FloatProcessorT2 source, final int source_width, final int source_height, final int target_width, final int target_height) {
1427 source.setPixels(source_width, source_height, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])source.getPixels(), source_width, source_height), 0.75f).data);
1428 source.resizeInPlace(target_width, target_height);
1429 return (byte[])source.convertToByte(false).getPixels(); // no scaling
1432 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1433 static final private byte[] meanResizeInHalf(final FloatProcessorT2 source, final int sourceWidth, final int sourceHeight, final int targetWidth, final int targetHeight) {
1434 final float[] sourceData = source.getFloatPixels();
1435 final float[] targetData = new float[targetWidth * targetHeight];
1436 int rs = 0;
1437 for (int r = 0; r < targetData.length; r += targetWidth) {
1438 int xs = -1;
1439 for (int x = 0; x < targetWidth; ++x)
1440 targetData[r + x] = sourceData[rs + ++xs] + sourceData[rs + ++xs];
1441 rs += sourceWidth;
1442 xs = -1;
1443 for (int x = 0; x < targetWidth; ++x) {
1444 targetData[r + x] += sourceData[rs + ++xs] + sourceData[rs + ++xs];
1445 targetData[r + x] /= 4;
1447 rs += sourceWidth;
1449 source.setPixels(targetWidth, targetHeight, targetData);
1450 return (byte[])source.convertToByte(false).getPixels();
1453 /** Queue/unqueue for mipmap removal on shutdown without saving. */
1454 public void queueForMipmapRemoval(final Patch p, boolean yes) {
1455 if (yes) touched_mipmaps.add(p);
1456 else touched_mipmaps.remove(p);
1459 /** Given an image and its source file name (without directory prepended), generate
1460 * a pyramid of images until reaching an image not smaller than 32x32 pixels.<br />
1461 * Such images are stored as jpeg 85% quality in a folder named trakem2.mipmaps.<br />
1462 * The Patch id and a ".jpg" extension will be appended to the filename in all cases.<br />
1463 * Any equally named files will be overwritten.
1465 public boolean generateMipMaps(final Patch patch) {
1466 //Utils.log2("mipmaps for " + patch);
1467 if (null == dir_mipmaps) createMipMapsDir(null);
1468 if (null == dir_mipmaps || isURL(dir_mipmaps)) return false;
1469 final String path = getAbsolutePath(patch);
1470 if (null == path) {
1471 Utils.log2("generateMipMaps: cannot find path for Patch " + patch);
1472 cannot_regenerate.add(patch);
1473 return false;
1475 synchronized (gm_lock) {
1476 gm_lock();
1477 if (hs_regenerating_mipmaps.contains(patch)) {
1478 // already being done
1479 gm_unlock();
1480 Utils.log2("Already being done: " + patch);
1481 return false;
1483 hs_regenerating_mipmaps.add(patch);
1484 gm_unlock();
1487 /** Record Patch as modified */
1488 touched_mipmaps.add(patch);
1490 /** Remove serialized features, if any */
1491 removeSerializedFeatures(patch);
1493 /** Remove serialized pointmatches, if any */
1494 removeSerializedPointMatches(patch);
1496 String srmode = patch.getProject().getProperty("image_resizing_mode");
1497 int resizing_mode = GAUSSIAN;
1498 if (null != srmode) resizing_mode = Loader.getMode(srmode);
1500 try {
1501 // Now:
1502 // 1 - Ask the Patch to apply a coordinate transform, or rather, create a function that gets the coordinate transform from the Patch and applies it to the 'ip'.
1503 // 2 - Then (1) should return both the transformed image and the alpha mask
1505 ImageProcessor ip;
1506 ByteProcessor alpha_mask = null;
1507 ByteProcessor outside_mask = null;
1508 final boolean coordinate_transformed;
1509 int type = patch.getType();
1511 // Obtain an image which may be coordinate-transformed, and an alpha mask.
1512 Patch.PatchImage pai = patch.createTransformedImage();
1513 ip = pai.target;
1514 alpha_mask = pai.mask; // can be null
1515 outside_mask = pai.outside; // can be null
1516 coordinate_transformed = pai.coordinate_transformed;
1517 pai = null;
1519 final String filename = new StringBuffer(new File(path).getName()).append('.').append(patch.getId()).append(".jpg").toString();
1520 int w = ip.getWidth();
1521 int h = ip.getHeight();
1523 // sigma = sqrt(2^level - 0.5^2)
1524 // where 0.5 is the estimated sigma for a full-scale image
1525 // which means sigma = 0.75 for the full-scale image (has level 0)
1526 // prepare a 0.75 sigma image from the original
1527 ColorModel cm = ip.getColorModel();
1528 int k = 0; // the scale level. Proper scale is: 1 / pow(2, k)
1529 // but since we scale 50% relative the previous, it's always 0.75
1531 // Set for the level 0 image, which is a duplicate of the one on the cache in any case
1532 ip.setMinAndMax(patch.getMin(), patch.getMax());
1535 // Proper support for LUT images: treat them as RGB
1536 if (ip.isColorLut()) {
1537 ip = ip.convertToRGB();
1538 cm = null;
1539 type = ImagePlus.COLOR_RGB;
1542 if (ImagePlus.COLOR_RGB == type) {
1543 // TODO releaseToFit proper
1544 releaseToFit(w * h * 4 * 5);
1545 final ColorProcessor cp = (ColorProcessor)ip;
1546 final FloatProcessorT2 red = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(0, red);
1547 final FloatProcessorT2 green = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(1, green);
1548 final FloatProcessorT2 blue = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(2, blue);
1549 FloatProcessorT2 alpha;
1550 final FloatProcessorT2 outside;
1551 if (null != alpha_mask) {
1552 alpha = new FloatProcessorT2((FloatProcessor)alpha_mask.convertToFloat());
1553 } else {
1554 alpha = null;
1556 if (null != outside_mask) {
1557 outside = new FloatProcessorT2((FloatProcessor)outside_mask.convertToFloat());
1558 if ( null == alpha ) {
1559 alpha = outside;
1560 alpha_mask = outside_mask;
1562 } else {
1563 outside = null;
1566 // sw,sh are the dimensions of the image to blur
1567 // w,h are the dimensions to scale the blurred image to
1568 int sw = w,
1569 sh = h;
1571 final String target_dir0 = getLevelDir(dir_mipmaps, 0);
1572 // No alpha channel:
1573 // - use gaussian resizing
1574 // - use standard ImageJ java.awt.Image creation
1576 // Generate level 0 first:
1577 // TODO Add alpha information into the int[] pixel array or make the image visible some ohter way
1578 if (!(null == alpha ? ini.trakem2.io.ImageSaver.saveAsJpeg(cp, target_dir0 + filename, 0.85f, false)
1579 : ini.trakem2.io.ImageSaver.saveAsJpegAlpha(createARGBImage(w, h, embedAlpha((int[])cp.getPixels(), (byte[])alpha_mask.getPixels(), null == outside ? null : (byte[])outside_mask.getPixels())), target_dir0 + filename, 0.85f))) {
1580 cannot_regenerate.add(patch);
1581 } else {
1582 do {
1583 // 1 - Prepare values for the next scaled image
1584 sw = w;
1585 sh = h;
1586 w /= 2;
1587 h /= 2;
1588 k++;
1589 // 2 - Check that the target folder for the desired scale exists
1590 final String target_dir = getLevelDir(dir_mipmaps, k);
1591 if (null == target_dir) continue;
1592 // 3 - Blur the previous image to 0.75 sigma, and scale it
1593 final byte[] r = gaussianBlurResizeInHalf(red, sw, sh, w, h); // will resize 'red' FloatProcessor in place.
1594 final byte[] g = gaussianBlurResizeInHalf(green, sw, sh, w, h); // idem
1595 final byte[] b = gaussianBlurResizeInHalf(blue, sw, sh, w, h); // idem
1596 final byte[] a = null == alpha ? null : gaussianBlurResizeInHalf(alpha, sw, sh, w, h); // idem
1597 if ( null != outside ) {
1598 final byte[] o;
1599 if (alpha != outside)
1600 o = gaussianBlurResizeInHalf(outside, sw, sh, w, h); // idem
1601 else
1602 o = a;
1603 // Remove all not completely inside pixels from the alphamask
1604 // If there was no alpha mask, alpha is the outside itself
1605 for (int i=0; i<o.length; i++) {
1606 if ( (o[i]&0xff) != 255 ) a[i] = 0; // TODO I am sure there is a bitwise operation to do this in one step. Some thing like: a[i] &= 127;
1610 // 4 - Compose ColorProcessor
1611 final int[] pix = new int[w * h];
1612 if (null == alpha) {
1613 for (int i=0; i<pix.length; i++) {
1614 pix[i] = 0xff000000 | ((r[i]&0xff)<<16) | ((g[i]&0xff)<<8) | (b[i]&0xff);
1616 final ColorProcessor cp2 = new ColorProcessor(w, h, pix);
1617 // 5 - Save as jpeg
1618 if (!ini.trakem2.io.ImageSaver.saveAsJpeg(cp2, target_dir + filename, 0.85f, false)) {
1619 cannot_regenerate.add(patch);
1620 break;
1622 } else {
1623 // LIKELY no need to set alpha raster later in createARGBImage ... TODO
1624 for (int i=0; i<pix.length; i++) {
1625 pix[i] = ((a[i]&0xff)<<24) | ((r[i]&0xff)<<16) | ((g[i]&0xff)<<8) | (b[i]&0xff);
1627 final BufferedImage bi_save = createARGBImage(w, h, pix);
1628 if (!ini.trakem2.io.ImageSaver.saveAsJpegAlpha(bi_save, target_dir + filename, 0.85f)) {
1629 cannot_regenerate.add(patch);
1630 bi_save.flush();
1631 break;
1633 bi_save.flush();
1635 } while (w >= 32 && h >= 32); // not smaller than 32x32
1637 } else {
1638 // Greyscale:
1639 releaseToFit(w * h * 4 * 5);
1640 final boolean as_grey = !ip.isColorLut();
1641 if (as_grey && null == cm) {
1642 cm = GRAY_LUT;
1645 if (Loader.GAUSSIAN == resizing_mode) {
1646 FloatProcessor fp = (FloatProcessor) ip.convertToFloat();
1647 int sw=w, sh=h;
1649 FloatProcessor alpha;
1650 FloatProcessor outside;
1651 if (null != alpha_mask) {
1652 alpha = new FloatProcessorT2((FloatProcessor)alpha_mask.convertToFloat());
1653 } else {
1654 alpha = null;
1656 if (null != outside_mask) {
1657 outside = new FloatProcessorT2((FloatProcessor)outside_mask.convertToFloat());
1658 if (null == alpha) {
1659 alpha = outside;
1660 alpha_mask = outside_mask;
1662 } else {
1663 outside = null;
1666 do {
1667 // 0 - blur the previous image to 0.75 sigma
1668 if (0 != k) { // not doing so at the end because it would add one unnecessary blurring
1669 fp = new FloatProcessorT2(sw, sh, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])fp.getPixels(), sw, sh), 0.75f).data, cm);
1670 if (null != alpha) {
1671 alpha = new FloatProcessorT2(sw, sh, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])alpha.getPixels(), sw, sh), 0.75f).data, null);
1672 if (alpha != outside && outside != null) {
1673 outside = new FloatProcessorT2(sw, sh, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])outside.getPixels(), sw, sh), 0.75f).data, null);
1677 // 1 - check that the target folder for the desired scale exists
1678 final String target_dir = getLevelDir(dir_mipmaps, k);
1679 if (null == target_dir) continue;
1680 // 2 - generate scaled image
1681 if (0 != k) {
1682 fp = (FloatProcessor)fp.resize(w, h);
1683 if (ImagePlus.GRAY8 == type) {
1684 fp.setMinAndMax(0, 255); // the min and max was expanded into 0,255 range at convertToFloat for 8-bit images, so the only limit to be added now to the FloatProcessor is that of the 8-bit range. The latter is done automatically for FloatProcessor class, but FloatProcessorT2 doesn't, to avoid the expensive (and here superfluous) operation of looping through all pixels in the findMinAndMax method.
1685 } else {
1686 fp.setMinAndMax(patch.getMin(), patch.getMax()); // Must be done: the resize doesn't preserve the min and max!
1688 if (null != alpha) {
1689 alpha = (FloatProcessor)alpha.resize(w, h);
1690 if (alpha != outside && null != outside) {
1691 outside = (FloatProcessor)outside.resize(w, h);
1695 if (null != alpha) {
1696 // 3 - save as jpeg with alpha
1697 final byte[] a = (byte[])alpha.convertToByte(false).getPixels();
1698 if (null != outside) {
1699 final byte[] o;
1700 if (alpha != outside) {
1701 o = (byte[])outside.convertToByte(false).getPixels();
1702 } else {
1703 o = a;
1705 // Remove all not completely inside pixels from the alpha mask
1706 // If there was no alpha mask, alpha is the outside itself
1707 for (int i=0; i<o.length; i++) {
1708 if ( (o[i]&0xff) != 255 ) a[i] = 0; // TODO I am sure there is a bitwise operation to do this in one step. Some thing like: a[i] &= 127;
1711 if (ImagePlus.GRAY8 != type) { // for 8-bit, the min,max has been applied when going to FloatProcessor
1712 fp.setMinAndMax(patch.getMin(), patch.getMax());
1714 final int[] pix = embedAlpha((int[])fp.convertToRGB().getPixels(), a);
1716 final BufferedImage bi_save = createARGBImage(w, h, pix);
1717 if (!ini.trakem2.io.ImageSaver.saveAsJpegAlpha(bi_save, target_dir + filename, 0.85f)) {
1718 cannot_regenerate.add(patch);
1719 bi_save.flush();
1720 break;
1722 bi_save.flush();
1723 } else {
1724 // 3 - save as 8-bit jpeg
1725 final ImageProcessor ip2 = Utils.convertTo(fp, type, false); // no scaling, since the conversion to float above didn't change the range. This is needed because of the min and max
1726 if (!coordinate_transformed) ip2.setMinAndMax(patch.getMin(), patch.getMax()); // Must be done, it's a new ImageProcessor
1727 if (null != cm) ip2.setColorModel(cm); // the LUT
1729 if (!ini.trakem2.io.ImageSaver.saveAsJpeg(ip2, target_dir + filename, 0.85f, as_grey)) {
1730 cannot_regenerate.add(patch);
1731 break;
1735 // 4 - prepare values for the next scaled image
1736 sw = w;
1737 sh = h;
1738 w /= 2;
1739 h /= 2;
1740 k++;
1741 } while (w >= 32 && h >= 32); // not smaller than 32x32
1743 } else {
1744 //final StopWatch timer = new StopWatch();
1746 // use java hardware-accelerated resizing
1747 Image awt = ip.createImage();
1749 BufferedImage balpha = null == alpha_mask ? null : convertToBufferedImage(alpha_mask);
1750 BufferedImage boutside = null == outside_mask ? null : convertToBufferedImage(outside_mask);
1752 BufferedImage bi = null;
1753 final Object hint = getHint(resizing_mode);
1755 ip = null;
1757 do {
1758 // check that the target folder for the desired scale exists
1759 final String target_dir = getLevelDir(dir_mipmaps, k);
1760 if (null == target_dir) continue;
1761 // obtain half image
1762 // for level 0 and others, when awt is not a BufferedImage or needs to be reduced in size (to new w,h)
1763 final BufferedImage[] res = IToBI(awt, w, h, hint, balpha, boutside);
1764 bi = res[0];
1765 balpha = res[1];
1766 boutside = res[2];
1767 // prepare next iteration
1768 if (awt != bi) awt.flush();
1769 awt = bi;
1770 w /= 2;
1771 h /= 2;
1772 k++;
1773 // save this iteration
1774 if ( ( (null != balpha || null != boutside) &&
1775 !ini.trakem2.io.ImageSaver.saveAsJpegAlpha(bi, target_dir + filename, 0.85f))
1776 || ( null == balpha && null == boutside && !ini.trakem2.io.ImageSaver.saveAsJpeg(bi, target_dir + filename, 0.85f, as_grey))) {
1777 cannot_regenerate.add(patch);
1778 break;
1780 } while (w >= 32 && h >= 32);
1781 bi.flush();
1783 //timer.cumulative();
1787 // flush any cached tiles
1788 flushMipMaps(patch.getId());
1790 return true;
1791 } catch (Throwable e) {
1792 IJError.print(e);
1793 cannot_regenerate.add(patch);
1794 return false;
1795 } finally {
1796 // gets executed even when returning from the catch statement or within the try/catch block
1797 synchronized (gm_lock) {
1798 gm_lock();
1799 hs_regenerating_mipmaps.remove(patch);
1800 gm_unlock();
1805 /** Remove the file, if it exists, with serialized features for patch.
1806 * Returns true when no such file or on success; false otherwise. */
1807 public boolean removeSerializedFeatures(final Patch patch) {
1808 final File f = new File(new StringBuffer(dir_storage).append("features.ser/features_").append(patch.getUniqueIdentifier()).append(".ser").toString());
1809 if (f.exists()) {
1810 try {
1811 f.delete();
1812 return true;
1813 } catch (Exception e) {
1814 IJError.print(e);
1815 return false;
1817 } else return true;
1820 /** Remove the file, if it exists, with serialized point matches for patch.
1821 * Returns true when no such file or on success; false otherwise. */
1822 public boolean removeSerializedPointMatches(final Patch patch) {
1823 boolean success = true;
1824 final File d = new File(new StringBuffer(dir_storage).append("pointmatches.ser").toString());
1825 if (d.exists()&&d.isDirectory()) {
1826 final String[] files = d.list();
1827 if ( files != null )
1829 for ( final String f : files ) {
1830 if ( f.matches( ".*_" + patch.getUniqueIdentifier() + "(_|\\.).*" ) ) {
1831 try {
1832 new File( d.getAbsolutePath() + "/" + f ).delete();
1833 } catch (Exception e) {
1834 IJError.print(e);
1835 success = false;
1841 return success;
1844 /** Generate image pyramids and store them into files under the dir_mipmaps for each Patch object in the Project. The method is multithreaded, using as many processors as available to the JVM.
1846 * @param al : the list of Patch instances to generate mipmaps for.
1847 * @param overwrite : whether to overwrite any existing mipmaps, or save only those that don't exist yet for whatever reason. This flag provides the means for minimal effort mipmap regeneration.)
1848 * */
1849 public Bureaucrat generateMipMaps(final ArrayList al, final boolean overwrite) {
1850 if (null == al || 0 == al.size()) return null;
1851 if (null == dir_mipmaps) createMipMapsDir(null);
1852 if (isURL(dir_mipmaps)) {
1853 Utils.log("Mipmaps folder is an URL, can't save files into it.");
1854 return null;
1856 final Worker worker = new Worker("Generating MipMaps") {
1857 public void run() {
1858 this.setAsBackground(true);
1859 this.startedWorking();
1860 try {
1862 final Worker wo = this;
1864 Utils.log2("starting mipmap generation ..");
1866 final int size = al.size();
1867 final Patch[] pa = new Patch[size];
1868 final Thread[] threads = MultiThreading.newThreads();
1869 al.toArray(pa);
1870 final AtomicInteger ai = new AtomicInteger(0);
1872 for (int ithread = 0; ithread < threads.length; ++ithread) {
1873 threads[ithread] = new Thread(new Runnable() {
1874 public void run() {
1876 for (int k = ai.getAndIncrement(); k < size; k = ai.getAndIncrement()) {
1877 if (wo.hasQuitted()) {
1878 return;
1880 wo.setTaskName("Generating MipMaps " + (k+1) + "/" + size);
1881 try {
1882 boolean ow = overwrite;
1883 if (!overwrite) {
1884 // check if all the files exist. If one doesn't, then overwrite all anyway
1885 int w = (int)pa[k].getWidth();
1886 int h = (int)pa[k].getHeight();
1887 int level = 0;
1888 final String filename = new File(getAbsolutePath(pa[k])).getName() + "." + pa[k].getId() + ".jpg";
1889 do {
1890 w /= 2;
1891 h /= 2;
1892 level++;
1893 if (!new File(dir_mipmaps + level + "/" + filename).exists()) {
1894 ow = true;
1895 break;
1897 } while (w >= 32 && h >= 32);
1899 if (!ow) continue;
1900 if ( ! generateMipMaps(pa[k]) ) {
1901 // some error ocurred
1902 Utils.log2("Could not generate mipmaps for patch " + pa[k]);
1904 } catch (Exception e) {
1905 IJError.print(e);
1912 MultiThreading.startAndJoin(threads);
1914 } catch (Exception e) {
1915 IJError.print(e);
1918 this.finishedWorking();
1921 return Bureaucrat.createAndStart(worker, ((Patch)al.get(0)).getProject());
1924 private final String getLevelDir(final String dir_mipmaps, final int level) {
1925 // synch, so that multithreaded generateMipMaps won't collide trying to create dirs
1926 synchronized (db_lock) {
1927 lock();
1928 final String path = new StringBuffer(dir_mipmaps).append(level).append('/').toString();
1929 if (isURL(dir_mipmaps)) {
1930 unlock();
1931 return path;
1933 final File file = new File(path);
1934 if (file.exists() && file.isDirectory()) {
1935 unlock();
1936 return path;
1938 // else, create it
1939 try {
1940 file.mkdir();
1941 unlock();
1942 return path;
1943 } catch (Exception e) {
1944 IJError.print(e);
1946 unlock();
1948 return null;
1951 /** If parent path is null, it's asked for.*/
1952 public boolean createMipMapsDir(String parent_path) {
1953 if (null == parent_path) {
1954 // try to create it in the same directory where the XML file is
1955 if (null != dir_storage) {
1956 File f = new File(dir_storage + "trakem2.mipmaps");
1957 if (!f.exists()) {
1958 try {
1959 if (f.mkdir()) {
1960 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
1961 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
1962 return true;
1964 } catch (Exception e) {}
1965 } else if (f.isDirectory()) {
1966 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
1967 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
1968 return true;
1970 // else can't use it
1971 } else if (null != project_file_path) {
1972 final File fxml = new File(project_file_path);
1973 final File fparent = fxml.getParentFile();
1974 if (null != fparent && fparent.isDirectory()) {
1975 File f = new File(fparent.getAbsolutePath().replace('\\', '/') + "/" + fxml.getName() + ".mipmaps");
1976 try {
1977 if (f.mkdir()) {
1978 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
1979 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
1980 return true;
1982 } catch (Exception e) {}
1985 // else, ask for a new folder
1986 final DirectoryChooser dc = new DirectoryChooser("Select MipMaps parent directory");
1987 parent_path = dc.getDirectory();
1988 if (null == parent_path) return false;
1989 if (!parent_path.endsWith("/")) parent_path += "/";
1991 // examine parent path
1992 final File file = new File(parent_path);
1993 if (file.exists()) {
1994 if (file.isDirectory()) {
1995 // all OK
1996 this.dir_mipmaps = parent_path + "trakem2.mipmaps/";
1997 try {
1998 File f = new File(this.dir_mipmaps);
1999 if (!f.exists()) {
2000 f.mkdir();
2002 } catch (Exception e) {
2003 IJError.print(e);
2004 return false;
2006 } else {
2007 Utils.showMessage("Selected parent path is not a directory. Please choose another one.");
2008 return createMipMapsDir(null);
2010 } else {
2011 Utils.showMessage("Parent path does not exist. Please select a new one.");
2012 return createMipMapsDir(null);
2014 return true;
2017 /** Remove all mipmap images from the cache, and optionally set the dir_mipmaps to null. */
2018 public void flushMipMaps(boolean forget_dir_mipmaps) {
2019 if (null == dir_mipmaps) return;
2020 synchronized (db_lock) {
2021 lock();
2022 if (forget_dir_mipmaps) this.dir_mipmaps = null;
2023 mawts.removeAllPyramids(); // does not remove level 0 awts (i.e. the 100% images)
2024 unlock();
2028 /** Remove from the cache all images of level larger than zero corresponding to the given patch id. */
2029 public void flushMipMaps(final long id) {
2030 if (null == dir_mipmaps) return;
2031 synchronized (db_lock) {
2032 lock();
2033 try {
2034 //mawts.removePyramid(id); // does not remove level 0 awts (i.e. the 100% images)
2035 // Need to remove ALL now, since level 0 is also included as a mipmap:
2036 for (final Image img : mawts.remove(id)) {
2037 if (null != img) img.flush();
2039 } catch (Exception e) { e.printStackTrace(); }
2040 unlock();
2044 /** Gets data from the Patch and queues a new task to do the file removal in a separate task manager thread. */
2045 public void removeMipMaps(final Patch p) {
2046 if (null == dir_mipmaps) return;
2047 try {
2048 final int width = (int)p.getWidth();
2049 final int height = (int)p.getHeight();
2050 final String path = getAbsolutePath(p);
2051 if (null == path) return; // missing file
2052 final String filename = new File(path).getName() + "." + p.getId() + ".jpg";
2053 // cue the task in a dispatcher:
2054 dispatcher.exec(new Runnable() { public void run() { // copy-paste as a replacement for (defmacro ... we luv java
2055 removeMipMaps(filename, width, height);
2056 }});
2057 } catch (Exception e) {
2058 IJError.print(e);
2062 private void removeMipMaps(final String filename, final int width, final int height) {
2063 int w = width;
2064 int h = height;
2065 int k = 0; // the level
2066 do {
2067 final File f = new File(dir_mipmaps + k + "/" + filename);
2068 if (f.exists()) {
2069 try {
2070 if (!f.delete()) {
2071 Utils.log2("Could not remove file " + f.getAbsolutePath());
2073 } catch (Exception e) {
2074 IJError.print(e);
2077 w /= 2;
2078 h /= 2;
2079 k++;
2080 } while (w >= 32 && h >= 32); // not smaller than 32x32
2083 /** Checks whether this Loader is using a directory of image pyramids for each Patch or not. */
2084 public boolean isMipMapsEnabled() {
2085 return null != dir_mipmaps;
2088 /** Return the closest level to @param level that exists as a file.
2089 * If no valid path is found for the patch, returns ERROR_PATH_NOT_FOUND.
2091 public int getClosestMipMapLevel(final Patch patch, int level) {
2092 if (null == dir_mipmaps) return 0;
2093 try {
2094 final String path = getAbsolutePath(patch);
2095 if (null == path) return ERROR_PATH_NOT_FOUND;
2096 final String filename = new File(path).getName() + ".jpg";
2097 if (isURL(dir_mipmaps)) {
2098 if (level <= 0) return 0;
2099 // choose the smallest dimension
2100 // find max level that keeps dim over 32 pixels
2101 final int lev = getHighestMipMapLevel(Math.min(patch.getWidth(), patch.getHeight()));
2102 if (level > lev) return lev;
2103 return level;
2104 } else {
2105 do {
2106 final File f = new File(new StringBuffer(dir_mipmaps).append(level).append('/').append(filename).toString());
2107 if (f.exists()) {
2108 return level;
2110 // try the next level
2111 level--;
2112 } while (level >= 0);
2114 } catch (Exception e) {
2115 IJError.print(e);
2117 return 0;
2120 /** A temporary list of Patch instances for which a pyramid is being generated. */
2121 final private HashSet hs_regenerating_mipmaps = new HashSet();
2123 /** A lock for the generation of mipmaps. */
2124 final private Object gm_lock = new Object();
2125 private boolean gm_locked = false;
2127 protected final void gm_lock() {
2128 //Utils.printCaller(this, 7);
2129 while (gm_locked) { try { gm_lock.wait(); } catch (InterruptedException ie) {} }
2130 gm_locked = true;
2132 protected final void gm_unlock() {
2133 //Utils.printCaller(this, 7);
2134 if (gm_locked) {
2135 gm_locked = false;
2136 gm_lock.notifyAll();
2140 /** Checks if the mipmap file for the Patch and closest upper level to the desired magnification exists. */
2141 public boolean checkMipMapFileExists(final Patch p, final double magnification) {
2142 if (null == dir_mipmaps) return false;
2143 final int level = getMipMapLevel(magnification, maxDim(p));
2144 if (isURL(dir_mipmaps)) return true; // just assume that it does
2145 if (new File(dir_mipmaps + level + "/" + new File(getAbsolutePath(p)).getName() + "." + p.getId() + ".jpg").exists()) return true;
2146 return false;
2149 final HashSet<Patch> cannot_regenerate = new HashSet<Patch>();
2151 /** Loads the file containing the scaled image corresponding to the given level (or the maximum possible level, if too large) and returns it as an awt.Image, or null if not found. Will also regenerate the mipmaps, i.e. recreate the pre-scaled jpeg images if they are missing. Does not frees memory on its own. */
2152 protected Image fetchMipMapAWT(final Patch patch, final int level) {
2153 if (null == dir_mipmaps) {
2154 Utils.log2("null dir_mipmaps");
2155 return null;
2157 try {
2158 // TODO should wait if the file is currently being generated
2159 // (it's somewhat handled by a double-try to open the jpeg image)
2161 final int max_level = getHighestMipMapLevel(patch);
2163 //Utils.log2("level is: " + max_level);
2165 final String filepath = getInternalFileName(patch);
2166 if (null == filepath) {
2167 Utils.log2("null filepath!");
2168 return null;
2170 final String path = new StringBuffer(dir_mipmaps).append( level > max_level ? max_level : level ).append('/').append(filepath).append('.').append(patch.getId()).append(".jpg").toString();
2171 Image img = null;
2173 if (patch.hasAlphaChannel()) {
2174 img = ImageSaver.openJpegAlpha(path);
2175 } else {
2176 switch (patch.getType()) {
2177 case ImagePlus.GRAY16:
2178 case ImagePlus.GRAY8:
2179 case ImagePlus.GRAY32:
2180 img = ImageSaver.openGreyJpeg(path);
2181 break;
2182 default:
2183 IJ.redirectErrorMessages();
2184 ImagePlus imp = opener.openImage(path); // considers URL as well
2185 if (null != imp) return patch.createImage(imp); // considers c_alphas
2186 //img = patch.adjustChannels(Toolkit.getDefaultToolkit().createImage(path)); // doesn't work
2187 //img = patch.adjustChannels(ImageSaver.openColorJpeg(path)); // doesn't work
2188 //Utils.log2("color jpeg path: "+ path);
2189 //Utils.log2("exists ? " + new File(path).exists());
2190 break;
2193 if (null != img) return img;
2196 // if we got so far ... try to regenerate the mipmaps
2197 if (!mipmaps_regen) {
2198 return null;
2201 // check that REALLY the file doesn't exist.
2202 if (cannot_regenerate.contains(patch)) {
2203 Utils.log("Cannot regenerate mipmaps for patch " + patch);
2204 return null;
2207 //Utils.log2("getMipMapAwt: imp is " + imp + " for path " + dir_mipmaps + level + "/" + new File(getAbsolutePath(patch)).getName() + "." + patch.getId() + ".jpg");
2209 // Regenerate in the case of not asking for an image under 32x32
2210 double scale = 1 / Math.pow(2, level);
2211 if (level >= 0 && patch.getWidth() * scale >= 32 && patch.getHeight() * scale >= 32 && isMipMapsEnabled()) {
2212 // regenerate
2213 synchronized (gm_lock) {
2214 gm_lock();
2216 if (hs_regenerating_mipmaps.contains(patch)) {
2217 // already being done
2218 gm_unlock();
2219 return null;
2221 // else, start it
2222 Worker worker = new Worker("Regenerating mipmaps") {
2223 public void run() {
2224 this.setAsBackground(true);
2225 this.startedWorking();
2226 try {
2227 generateMipMaps(patch);
2228 } catch (Exception e) {
2229 IJError.print(e);
2232 Display.repaint(patch.getLayer(), patch, 0);
2234 this.finishedWorking();
2237 Bureaucrat burro = Bureaucrat.create(worker, patch.getProject());
2238 burro.goHaveBreakfast();
2240 gm_unlock();
2242 return null;
2244 } catch (Exception e) {
2245 IJError.print(e);
2247 return null;
2250 /** Compute the number of bytes that the ImagePlus of a Patch will take. Assumes a large header of 1024 bytes. If the image is saved as a grayscale jpeg the returned bytes will be 5 times as expected, because jpeg images are opened as int[] and then copied to a byte[] if all channels have the same values for all pixels. */ // The header is unnecessary because it's read, but not stored except for some of its variables; it works here as a safety buffer space.
2251 public long estimateImageFileSize(final Patch p, final int level) {
2252 if (level > 0) {
2253 // jpeg image to be loaded:
2254 final double scale = 1 / Math.pow(2, level);
2255 return (long)(p.getWidth() * scale * p.getHeight() * scale * 5 + 1024);
2257 long size = (long)(p.getWidth() * p.getHeight());
2258 int bytes_per_pixel = 1;
2259 final int type = p.getType();
2260 switch (type) {
2261 case ImagePlus.GRAY32:
2262 bytes_per_pixel = 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
2263 break;
2264 case ImagePlus.GRAY16:
2265 bytes_per_pixel = 3; // 2 for the ShortProcessor, and 1 for the pixels8
2266 case ImagePlus.COLOR_RGB:
2267 bytes_per_pixel = 4;
2268 break;
2269 case ImagePlus.GRAY8:
2270 case ImagePlus.COLOR_256:
2271 bytes_per_pixel = 1;
2272 // check jpeg, which can only encode RGB (taken care of above) and 8-bit and 8-bit color images:
2273 String path = ht_paths.get(p.getId());
2274 if (null != path && path.endsWith(".jpg")) bytes_per_pixel = 5; //4 for the int[] and 1 for the byte[]
2275 break;
2276 default:
2277 bytes_per_pixel = 5; // conservative
2278 break;
2281 return size * bytes_per_pixel + 1024;
2284 public String makeProjectName() {
2285 if (null == project_file_path || 0 == project_file_path.length()) return super.makeProjectName();
2286 final String name = new File(project_file_path).getName();
2287 final int i_dot = name.lastIndexOf('.');
2288 if (-1 == i_dot) return name;
2289 if (0 == i_dot) return super.makeProjectName();
2290 return name.substring(0, i_dot);
2294 /** Returns the path where the imp is saved to: the storage folder plus a name. */
2295 public String handlePathlessImage(final ImagePlus imp) {
2296 final FileInfo fi = imp.getOriginalFileInfo();
2297 if (null == fi.fileName || fi.fileName.equals("")) {
2298 fi.fileName = "img_" + System.currentTimeMillis() + ".tif";
2300 if (!fi.fileName.endsWith(".tif")) fi.fileName += ".tif";
2301 fi.directory = dir_storage;
2302 if (imp.getNSlices() > 1) {
2303 new FileSaver(imp).saveAsTiffStack(dir_storage + fi.fileName);
2304 } else {
2305 new FileSaver(imp).saveAsTiff(dir_storage + fi.fileName);
2307 Utils.log2("Saved a copy into the storage folder:\n" + dir_storage + fi.fileName);
2308 return dir_storage + fi.fileName;
2311 /** Generates layer-wise mipmaps with constant tile width and height. The mipmaps include only images.
2312 * Mipmaps area generated all the way down until the entire canvas fits within one single tile.
2314 public Bureaucrat generateLayerMipMaps(final Layer[] la, final int starting_level) {
2315 // hard-coded dimensions for layer mipmaps.
2316 final int WIDTH = 512;
2317 final int HEIGHT = 512;
2319 // Each tile needs some coding system on where it belongs. For example in its file name, such as <layer_id>_Xi_Yi
2321 // Generate the starting level mipmaps, and then the others from it by gaussian or whatever is indicated in the project image_resizing_mode property.
2322 return null;