Working version without multi-threading and bureaucrats
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / persistence / FSLoader.java
blob9580a4e37459cb7fe172859d9b0025b5b6f5b65d
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 /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.ImageStack;
28 import ij.VirtualStack;
29 import ij.gui.YesNoCancelDialog;
30 import ij.io.DirectoryChooser;
31 import ij.io.FileInfo;
32 import ij.io.FileSaver;
33 import ij.io.OpenDialog;
34 import ij.io.Opener;
35 import ij.plugin.filter.GaussianBlur;
36 import ij.process.ByteProcessor;
37 import ij.process.ColorProcessor;
38 import ij.process.FloatProcessor;
39 import ij.process.ImageProcessor;
40 import ini.trakem2.ControlWindow;
41 import ini.trakem2.Project;
42 import ini.trakem2.display.DLabel;
43 import ini.trakem2.display.Display;
44 import ini.trakem2.display.Displayable;
45 import ini.trakem2.display.Layer;
46 import ini.trakem2.display.MipMapImage;
47 import ini.trakem2.display.Patch;
48 import ini.trakem2.display.Stack;
49 import ini.trakem2.imaging.FloatProcessorT2;
50 import ini.trakem2.imaging.P;
51 import ini.trakem2.io.ImageSaver;
52 import ini.trakem2.io.RagMipMaps;
53 import ini.trakem2.io.RawMipMaps;
54 import ini.trakem2.utils.Bureaucrat;
55 import ini.trakem2.utils.CachingThread;
56 import ini.trakem2.utils.IJError;
57 import ini.trakem2.utils.Utils;
58 import ini.trakem2.utils.Worker;
60 import java.awt.Image;
61 import java.awt.event.ActionEvent;
62 import java.awt.event.ActionListener;
63 import java.awt.event.KeyEvent;
64 import java.awt.geom.AffineTransform;
65 import java.awt.geom.Area;
66 import java.awt.image.BufferedImage;
67 import java.awt.image.PixelGrabber;
68 import java.io.BufferedInputStream;
69 import java.io.File;
70 import java.io.FileInputStream;
71 import java.io.FilenameFilter;
72 import java.io.InputStream;
73 import java.util.ArrayList;
74 import java.util.Collection;
75 import java.util.Collections;
76 import java.util.HashMap;
77 import java.util.HashSet;
78 import java.util.List;
79 import java.util.Map;
80 import java.util.Set;
81 import java.util.concurrent.Callable;
82 import java.util.concurrent.ExecutionException;
83 import java.util.concurrent.ExecutorService;
84 import java.util.concurrent.Executors;
85 import java.util.concurrent.Future;
86 import java.util.concurrent.ScheduledExecutorService;
87 import java.util.concurrent.TimeUnit;
88 import java.util.concurrent.TimeoutException;
89 import java.util.concurrent.atomic.AtomicInteger;
90 import java.util.regex.Pattern;
91 import java.util.zip.GZIPInputStream;
93 import javax.swing.JMenu;
94 import javax.swing.JMenuItem;
95 import javax.swing.KeyStroke;
96 import javax.xml.parsers.SAXParser;
97 import javax.xml.parsers.SAXParserFactory;
99 import mpicbg.trakem2.transform.CoordinateTransform;
100 import net.imglib2.img.Img;
101 import net.imglib2.img.array.ArrayImgs;
102 import net.imglib2.img.imageplus.FloatImagePlus;
103 import net.imglib2.img.imageplus.ImagePlusImgs;
104 import net.imglib2.type.numeric.real.FloatType;
106 import org.janelia.intensity.LinearIntensityMap;
107 import org.xml.sax.InputSource;
110 /** 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. */
111 public final class FSLoader extends Loader {
113 /* sigma of the Gaussian kernel sto be used for downsampling by a factor of 2 */
114 final private static double SIGMA_2 = Math.sqrt( 0.75 );
116 /** Largest id seen so far. */
117 private long max_id = -1;
118 /** Largest blob ID seen so far. First valid ID will equal 1. */
119 private long max_blob_id = 0;
121 private final Map<Long,String> ht_paths = Collections.synchronizedMap(new HashMap<Long,String>());
122 /** For saving and overwriting. */
123 private String project_file_path = null;
124 /** Path to the directory hosting the file image pyramids. */
125 private String dir_mipmaps = null;
126 /** Path to the directory the user provided when creating the project. */
127 private String dir_storage = null;
128 /** Path to the directory hosting the alpha masks. */
129 private String dir_masks = null;
131 /** Path to dir_storage + "trakem2.images/" */
132 private String dir_image_storage = null;
134 private Set<Patch> touched_mipmaps = Collections.synchronizedSet(new HashSet<Patch>());
136 private Set<Patch> mipmaps_to_remove = Collections.synchronizedSet(new HashSet<Patch>());
138 /** Used to open a project from an existing XML file. */
139 public FSLoader() {
140 super(); // register
141 FSLoader.startStaticServices();
144 private String unuid = null;
146 /** Used to create a new project, NOT from an XML file.
147 * Throws an Exception if the loader cannot read and write to the storage folder. */
148 public FSLoader(final String storage_folder) throws Exception {
149 this();
150 if (null == storage_folder) this.dir_storage = super.getStorageFolder(); // home dir
151 else this.dir_storage = storage_folder;
152 this.dir_storage = this.dir_storage.replace('\\', '/');
153 if (!this.dir_storage.endsWith("/")) this.dir_storage += "/";
154 if (!Loader.canReadAndWriteTo(dir_storage)) {
155 Utils.log("WARNING can't read/write to the storage_folder at " + dir_storage);
156 throw new Exception("Can't write to storage folder " + dir_storage);
157 } else {
158 this.unuid = createUNUId(this.dir_storage);
159 createMipMapsDir(this.dir_storage);
160 crashDetector();
164 private String createUNUId(String dir_storage) {
165 synchronized (db_lock) {
166 try {
167 if (null == dir_storage) dir_storage = System.getProperty("user.dir") + "/";
168 return new StringBuilder(64).append(System.currentTimeMillis()).append('.')
169 .append(Math.abs(dir_storage.hashCode())).append('.')
170 .append(Math.abs(System.getProperty("user.name").hashCode()))
171 .toString();
172 } catch (Exception e) {
173 IJError.print(e);
176 return null;
179 /** 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. */
180 private void crashDetector() {
181 if (null == dir_mipmaps) {
182 Utils.log2("Could NOT create crash detection system: null dir_mipmaps.");
183 return;
185 File f = new File(dir_mipmaps + ".open.t2");
186 Utils.log2("Crash detector file is " + dir_mipmaps + ".open.t2");
187 try {
188 if (f.exists()) {
189 // crashed!
190 notifyMipMapsOutOfSynch();
191 } else {
192 if (!f.createNewFile() && !dir_mipmaps.startsWith("http:")) {
193 Utils.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
194 } else {
195 Utils.log2("Created crash detection system.");
198 } catch (Exception e) {
199 Utils.log2("Crash detector error:" + e);
200 IJError.print(e);
204 public String getProjectXMLPath() {
205 if (null == project_file_path) return null;
206 return project_file_path.toString(); // a copy of it
209 /** Return the folder selected by a user to store files into; it's also the parent folder of the UNUId folder, and the recommended folder to store the XML file into. */
210 public String getStorageFolder() {
211 if (null == dir_storage) return super.getStorageFolder(); // the user's home
212 return dir_storage.toString(); // a copy
215 /** Returns a folder proven to be writable for images can be stored into. */
216 public String getImageStorageFolder() {
217 if (null == dir_image_storage) {
218 String s = getUNUIdFolder() + "trakem2.images/";
219 File f = new File(s);
220 if (f.exists() && f.isDirectory() && f.canWrite()) {
221 dir_image_storage = s;
222 return dir_image_storage;
224 else {
225 try {
226 f.mkdirs();
227 dir_image_storage = s;
228 } catch (Exception e) {
229 e.printStackTrace();
230 return getStorageFolder(); // fall back
234 return dir_image_storage;
237 /** Returns TMLHandler.getProjectData() . If the path is null it'll be asked for. */
238 public Object[] openFSProject(String path, final boolean open_displays) {
239 // clean path of double-slashes, safely (and painfully)
240 if (null != path) {
241 path = path.replace('\\','/');
242 path = path.trim();
243 int itwo = path.indexOf("//");
244 while (-1 != itwo) {
245 if (0 == itwo /* samba disk */
246 || (5 == itwo && "http:".equals(path.substring(0, 5)))) {
247 // do nothing
248 } else {
249 path = path.substring(0, itwo) + path.substring(itwo+1);
251 itwo = path.indexOf("//", itwo+1);
255 if (null == path) {
256 OpenDialog od = new OpenDialog("Select Project", OpenDialog.getDefaultDirectory(), null);
257 String file = od.getFileName();
258 if (null == file || file.toLowerCase().startsWith("null")) return null;
259 String dir = od.getDirectory().replace('\\', '/');
260 if (!dir.endsWith("/")) dir += "/";
261 this.project_file_path = dir + file;
262 Utils.log2("project file path 1: " + this.project_file_path);
263 } else {
264 this.project_file_path = path;
265 Utils.log2("project file path 2: " + this.project_file_path);
267 Utils.log2("Loader.openFSProject: path is " + path);
268 // check if any of the open projects uses the same file path, and refuse to open if so:
269 if (null != FSLoader.getOpenProject(project_file_path, this)) {
270 Utils.showMessage("The project is already open.");
271 return null;
274 Object[] data = null;
276 // parse file, according to expected format as indicated by the extension:
277 final String lcFilePath = this.project_file_path.toLowerCase();
278 if (lcFilePath.matches(".*(\\.xml|\\.xml\\.gz)")) {
279 InputStream i_stream = null;
280 TMLHandler handler = new TMLHandler(this.project_file_path, this);
281 if (handler.isUnreadable()) {
282 handler = null;
283 } else {
284 try {
285 SAXParserFactory factory = SAXParserFactory.newInstance();
286 factory.setValidating(false);
287 factory.setXIncludeAware(false);
288 SAXParser parser = factory.newSAXParser();
289 if (isURL(this.project_file_path)) {
290 i_stream = new java.net.URL(this.project_file_path).openStream();
291 } else {
292 i_stream = new BufferedInputStream(new FileInputStream(this.project_file_path));
294 if (lcFilePath.endsWith(".gz")) {
295 i_stream = new GZIPInputStream(i_stream);
297 InputSource input_source = new InputSource(i_stream);
298 parser.parse(input_source, handler);
299 } catch (java.io.FileNotFoundException fnfe) {
300 Utils.log("ERROR: File not found: " + path);
301 handler = null;
302 } catch (Exception e) {
303 IJError.print(e);
304 handler = null;
305 } finally {
306 if (null != i_stream) {
307 try {
308 i_stream.close();
309 } catch (Exception e) {
310 IJError.print(e);
315 if (null == handler) {
316 Utils.showMessage("Error when reading the project .xml file.");
317 return null;
320 data = handler.getProjectData(open_displays);
323 if (null == data) {
324 Utils.showMessage("Error when parsing the project .xml file.");
325 return null;
327 // else, good
328 crashDetector();
329 return data;
332 // Only one thread at a time may access this method.
333 synchronized static private final Project getOpenProject(final String project_file_path, final Loader caller) {
334 if (null == v_loaders) return null;
335 final Loader[] lo = (Loader[])v_loaders.toArray(new Loader[0]); // atomic way to get the list of loaders
336 for (int i=0; i<lo.length; i++) {
337 if (lo[i].equals(caller)) continue;
338 if (lo[i] instanceof FSLoader) {
339 if (null == ((FSLoader)lo[i]).project_file_path) continue; // not saved
340 if (((FSLoader)lo[i]).project_file_path.equals(project_file_path)) {
341 return Project.findProject(lo[i]);
345 return null;
348 static public final Project getOpenProject(final String project_file_path) {
349 return getOpenProject(project_file_path, null);
352 static public final int nStaticServiceThreads() {
353 int np = Runtime.getRuntime().availableProcessors();
354 // 1 core = 1 thread
355 // 2 cores = 2 threads
356 // 3+ cores = cores-1 threads
357 if (np > 2) np -= 1;
358 return np;
361 /** Restart the ExecutorService for mipmaps with {@param n_threads}. */
362 static public final void restartMipMapThreads(final int n_threads) {
363 if (null != regenerator && !regenerator.isShutdown()) {
364 regenerator.shutdown();
366 regenerator = Utils.newFixedThreadPool(Math.max(1, n_threads), "regenerator");
367 Utils.logAll("Restarted mipmap Executor Service for all projects with " + n_threads + " threads.");
370 static private void startStaticServices() {
371 // Up to nStaticServiceThreads for regenerator and repainter
372 if (null == regenerator || regenerator.isShutdown()) {
373 regenerator = Utils.newFixedThreadPool(1, "regenerator");
375 if (null == repainter || repainter.isShutdown()) {
376 repainter = Utils.newFixedThreadPool(nStaticServiceThreads, "repainter"); // for SnapshotPanel
378 // Maximum 2 threads for removing files
379 if (null == remover || remover.isShutdown()) {
380 remover = Utils.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors()), "mipmap remover");
382 // Just one thread for autosaver
383 if (null == autosaver || autosaver.isShutdown()) autosaver = Executors.newScheduledThreadPool(1);
386 /** Shutdown the various thread pools and disactivate services in general. */
387 static private void destroyStaticServices() {
388 if (null != regenerator) regenerator.shutdownNow();
389 if (null != remover) remover.shutdownNow();
390 if (null != repainter) repainter.shutdownNow();
391 if (null != autosaver) autosaver.shutdownNow();
394 @Override
395 public synchronized void destroy() {
396 super.destroy();
397 Utils.showStatus("", false);
398 // delete mipmap files that where touched and not cleared as saved (i.e. the project was not saved)
399 touched_mipmaps.addAll(mipmaps_to_remove);
400 Set<Patch> touched = new HashSet<Patch>();
401 synchronized (touched_mipmaps) {
402 touched.addAll(touched_mipmaps);
404 for (final Patch p : touched) {
405 File f = new File(getAbsolutePath(p)); // with slice info appended
406 //Utils.log2("File f is " + f);
407 Utils.log2("Removing mipmaps for " + p);
408 // Cannot run in the remover: is a daemon, and would be interrupted.
409 removeMipMaps(createIdPath(Long.toString(p.getId()), f.getName(), mExt), (int)p.getWidth(), (int)p.getHeight());
412 // remove empty trakem2.mipmaps folder if any
413 if (null != dir_mipmaps && !dir_mipmaps.equals(dir_storage)) {
414 File f = new File(dir_mipmaps);
415 if (f.isDirectory() && 0 == f.list(new FilenameFilter() {
416 public boolean accept(File fdir, String name) {
417 File file = new File(dir_mipmaps + name);
418 if (file.isHidden() || '.' == name.charAt(0)) return false;
419 return true;
421 }).length) {
422 try { f.delete(); } catch (Exception e) { Utils.log("Could not remove empty trakem2.mipmaps directory."); }
425 // remove crash detector
426 try {
427 File fm = new File(dir_mipmaps + ".open.t2");
428 if (!fm.delete()) {
429 Utils.log2("WARNING: could not delete crash detector file .open.t2 from trakem2.mipmaps folder at " + dir_mipmaps);
431 } catch (Exception e) {
432 Utils.log2("WARNING: crash detector file trakem.mipmaps/.open.t2 may NOT have been deleted.");
433 IJError.print(e);
435 if (null == ControlWindow.getProjects() || 1 == ControlWindow.getProjects().size()) {
436 destroyStaticServices();
438 // remove unuid dir if xml_path is empty (i.e. never saved and not opened from an .xml file)
439 if (null == project_file_path) {
440 Utils.log2("Removing unuid dir, since project was never saved.");
441 final File f = new File(getUNUIdFolder());
442 if (null != dir_mipmaps) Utils.removePrefixedFiles(f, "trakem2.mipmaps", null);
443 if (null != dir_masks) Utils.removePrefixedFiles(f, "trakem2.masks", null);
444 Utils.removePrefixedFiles(f, "features.ser", null);
445 Utils.removePrefixedFiles(f, "pointmatches.ser", null);
446 // Only if empty:
447 if (f.isDirectory()) {
448 try {
449 if (!f.delete()) {
450 Utils.log2("Could not delete unuid directory: likely not empty!");
452 } catch (Exception e) {
453 Utils.log2("Could not delete unuid directory: " + e);
459 /** Get the next unique id, not shared by any other object within the same project. */
460 @Override
461 public long getNextId() {
462 long nid = -1;
463 synchronized (db_lock) {
464 nid = ++max_id;
466 return nid;
469 /** Get the next unique id to be used for the {@link Patch}'s {@link CoordinateTransform} or alpha mask. */
470 @Override
471 public long getNextBlobId() {
472 long nid = 0;
473 synchronized (db_lock) {
474 nid = ++max_blob_id;
476 return nid;
479 /** Loaded in full from XML file */
480 public double[][][] fetchBezierArrays(long id) {
481 return null;
484 /** Loaded in full from XML file */
485 public ArrayList<?> fetchPipePoints(long id) {
486 return null;
489 /** Loaded in full from XML file */
490 public ArrayList<?> fetchBallPoints(long id) {
491 return null;
494 /** Loaded in full from XML file */
495 public Area fetchArea(long area_list_id, long layer_id) {
496 return null;
499 /* Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImagePlus.getProcessor(). */
500 public ImagePlus fetchImagePlus(final Patch p) {
501 return (ImagePlus)fetchImage(p, Layer.IMAGEPLUS);
504 /** Fetch the ImageProcessor in a synchronized manner, so that there are no conflicts in retrieving the ImageProcessor for a specific stack slice, for example.
505 * Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImageProcessor. */
506 public ImageProcessor fetchImageProcessor(final Patch p) {
507 return (ImageProcessor)fetchImage(p, Layer.IMAGEPROCESSOR);
510 /** So far accepts Layer.IMAGEPLUS and Layer.IMAGEPROCESSOR as format. */
511 public Object fetchImage(final Patch p, final int format) {
512 ImagePlus imp = null;
513 ImageProcessor ip = null;
514 String slice = null;
515 String path = null;
516 long n_bytes = 0;
517 ImageLoadingLock plock = null;
518 synchronized (db_lock) {
519 try {
520 imp = mawts.get(p.getId());
521 path = getAbsolutePath(p);
522 int i_sl = -1;
523 if (null != path) i_sl = path.lastIndexOf("-----#slice=");
524 if (-1 != i_sl) {
525 if (null != imp) {
526 // check that the stack is large enough (user may have changed it)
527 final int ia = Integer.parseInt(path.substring(i_sl + 12));
528 if (ia <= imp.getNSlices()) {
529 if (null == imp.getStack() || null == imp.getStack().getPixels(ia)) {
530 // reload (happens when closing a stack that was opened before importing it, and then trying to paint, for example)
531 mawts.removeImagePlus(p.getId());
532 imp = null;
533 } else {
534 imp.setSlice(ia);
535 switch (format) {
536 case Layer.IMAGEPROCESSOR:
537 ip = imp.getStack().getProcessor(ia);
538 return ip;
539 case Layer.IMAGEPLUS:
540 return imp;
541 default:
542 Utils.log("FSLoader.fetchImage: Unknown format " + format);
543 return null;
546 } else {
547 return null; // beyond bonds!
551 // for non-stack images
552 if (null != imp) {
553 switch (format) {
554 case Layer.IMAGEPROCESSOR:
555 return imp.getProcessor();
556 case Layer.IMAGEPLUS:
557 return imp;
558 default:
559 Utils.log("FSLoader.fetchImage: Unknown format " + format);
560 return null;
563 if (-1 != i_sl) {
564 slice = path.substring(i_sl);
565 // set path proper
566 path = path.substring(0, i_sl);
569 plock = getOrMakeImageLoadingLock(path);
570 } catch (Throwable t) {
571 handleCacheError(t);
572 return null;
576 synchronized (plock) {
577 imp = mawts.get(p.getId());
578 if (null == imp && !p.isPreprocessed()) {
579 // Try shared ImagePlus cache
580 imp = mawts.get(path); // could have been loaded by a different Patch that uses the same path,
581 // such as other slices of a stack or duplicated images.
582 if (null != imp) {
583 mawts.put(p.getId(), imp, (int)Math.max(p.getWidth(), p.getHeight()));
586 if (null != imp) {
587 // was loaded by a different thread, or is shareable
588 switch (format) {
589 case Layer.IMAGEPROCESSOR:
590 if (null != slice) {
591 return imp.getStack().getProcessor(Integer.parseInt(slice.substring(12)));
592 } else {
593 return imp.getProcessor();
595 case Layer.IMAGEPLUS:
596 if (null != slice) {
597 imp.setSlice(Integer.parseInt(slice.substring(12)));
599 return imp;
600 default:
601 Utils.log("FSLoader.fetchImage: Unknown format " + format);
602 return null;
606 // going to load:
608 // reserve memory:
609 n_bytes = estimateImageFileSize(p, 0);
610 releaseToFit(n_bytes);
611 imp = openImage(path);
613 preProcess(p, imp, n_bytes);
615 synchronized (db_lock) {
616 try {
617 if (null == imp) {
618 if (!hs_unloadable.contains(p)) {
619 Utils.log("FSLoader.fetchImagePlus: no image exists for patch " + p + " at path " + path);
620 hs_unloadable.add(p);
622 if (ControlWindow.isGUIEnabled()) {
623 FilePathRepair.add(p);
625 removeImageLoadingLock(plock);
626 return null;
628 if (null != slice) {
629 // set proper active slice
630 final int ia = Integer.parseInt(slice.substring(12));
631 imp.setSlice(ia);
632 if (Layer.IMAGEPROCESSOR == format) ip = imp.getStack().getProcessor(ia); // otherwise creates one new for nothing
633 } else {
634 // for non-stack images
635 // OBSOLETE and wrong //p.putMinAndMax(imp); // non-destructive contrast: min and max -- WRONG, it's destructive for ColorProcessor and ByteProcessor!
636 // puts the Patch min and max values into the ImagePlus processor.
637 if (Layer.IMAGEPROCESSOR == format) ip = imp.getProcessor();
639 mawts.put(p.getId(), imp, (int)Math.max(p.getWidth(), p.getHeight()));
640 // imp is cached, so:
641 removeImageLoadingLock(plock);
643 } catch (Exception e) {
644 IJError.print(e);
646 switch (format) {
647 case Layer.IMAGEPROCESSOR:
648 return ip; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
649 case Layer.IMAGEPLUS:
650 return imp;
651 default:
652 Utils.log("FSLoader.fetchImage: Unknown format " + format);
653 return null;
660 /** Returns the alpha mask image from a file, or null if none stored. */
661 @Override
662 public ByteProcessor fetchImageMask(final Patch p) {
663 return p.getAlphaMask();
666 @Override
667 synchronized public final String getMasksFolder() {
668 if (null == dir_masks) createMasksFolder();
669 return dir_masks;
672 synchronized private final void createMasksFolder() {
673 if (null == dir_masks) dir_masks = getUNUIdFolder() + "trakem2.masks/";
674 final File f = new File(dir_masks);
675 if (f.exists() && f.isDirectory()) return;
676 try {
677 f.mkdirs();
678 } catch (Exception e) {
679 IJError.print(e);
683 private String dir_cts = null;
685 @Override
686 synchronized public final String getCoordinateTransformsFolder() {
687 if (null == dir_cts) createCoordinateTransformsFolder();
688 return dir_cts;
691 synchronized private final void createCoordinateTransformsFolder() {
692 if (null == dir_cts) dir_cts = getUNUIdFolder() + "trakem2.cts/";
693 final File f = new File(dir_cts);
694 if (f.exists() && f.isDirectory()) return;
695 try {
696 f.mkdirs();
697 } catch (Exception e) {
698 IJError.print(e);
702 /** Loaded in full from XML file */
703 public Object[] fetchLabel(DLabel label) {
704 return null;
707 /** Loads and returns the original image, which is not cached, or returns null if it's not different than the working image. */
708 synchronized public ImagePlus fetchOriginal(final Patch patch) {
709 String original_path = patch.getOriginalPath();
710 if (null == original_path) return null;
711 // else, reserve memory and open it:
712 releaseToFit(estimateImageFileSize(patch, 0));
713 try {
714 return openImage(original_path);
715 } catch (Throwable t) {
716 IJError.print(t);
718 return null;
721 /* GENERIC, from DBObject calls. Records the id of the object in the HashMap ht_dbo.
722 * Always returns true. Does not check if another object has the same id.
724 public boolean addToDatabase(final DBObject ob) {
725 synchronized (db_lock) {
726 setChanged(true);
727 final long id = ob.getId();
728 if (id > max_id) {
729 max_id = id;
731 if (ob.getClass() == Patch.class) {
732 final Patch p = (Patch)ob;
733 if (p.hasCoordinateTransform()) {
734 max_blob_id = Math.max(p.getCoordinateTransformId(), max_blob_id);
736 if (p.hasAlphaMask()) {
737 max_blob_id = Math.max(p.getAlphaMaskId(), max_blob_id);
741 return true;
744 public boolean updateInDatabase(final DBObject ob, final String key) {
745 // Should only be GUI-driven
746 setChanged(true);
748 if (ob.getClass() == Patch.class) {
749 Patch p = (Patch)ob;
750 if (key.equals("tiff_working")) return null != setImageFile(p, fetchImagePlus(p));
752 return true;
755 public boolean updateInDatabase(final DBObject ob, final Set<String> keys) {
756 // Should only be GUI-driven
757 setChanged(true);
758 if (ob.getClass() == Patch.class) {
759 Patch p = (Patch)ob;
760 if (keys.contains("tiff_working")) return null != setImageFile(p, fetchImagePlus(p));
762 return true;
765 public boolean removeFromDatabase(final DBObject ob) {
766 synchronized (db_lock) {
767 setChanged(true);
768 // remove from the hashtable
769 final long loid = ob.getId();
770 Utils.log2("removing " + Project.getName(ob.getClass()) + " " + ob);
771 if (ob.getClass() == Patch.class) {
772 try {
773 // STRATEGY change: images are not owned by the FSLoader.
774 Patch p = (Patch)ob;
775 if (!ob.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p);
776 ht_paths.remove(p.getId()); // after removeMipMaps !
777 mawts.remove(loid);
778 cannot_regenerate.remove(p);
779 flushMipMaps(p.getId()); // locks on its own
780 touched_mipmaps.remove(p);
781 return true;
782 } catch (Throwable t) {
783 handleCacheError(t);
787 return true;
790 /** 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.
791 * If the Patch p current image path is different than its original image path, then the file is overwritten if it exists already.
793 public String setImageFile(final Patch p, final ImagePlus imp) {
794 if (null == imp) return null;
795 try {
796 String path = getAbsolutePath(p);
797 String slice = null;
799 // path can be null if the image is pasted, or from a copy, or totally new
800 if (null != path) {
801 int i_sl = path.lastIndexOf("-----#slice=");
802 if (-1 != i_sl) {
803 slice = path.substring(i_sl);
804 path = path.substring(0, i_sl);
806 } else {
807 // no path, inspect image FileInfo's path if the image has no changes
808 if (!imp.changes) {
809 final FileInfo fi = imp.getOriginalFileInfo();
810 if (null != fi && null != fi.directory && null != fi.fileName) {
811 final String fipath = fi.directory.replace('\\', '/') + "/" + fi.fileName;
812 if (new File(fipath).exists()) {
813 // no need to save a new image, it exists and has no changes
814 updatePaths(p, fipath, null != slice);
815 cacheAll(p, imp);
816 Utils.log2("Reusing image file: path exists for fileinfo at " + fipath);
817 return fipath;
822 if (null != path) {
823 final String starting_path = path;
824 // Save as a separate image in a new path within the storage folder
826 String filename = path.substring(path.lastIndexOf('/') +1);
828 //Utils.log2("filename 1: " + filename);
830 // remove .tif extension if there
831 if (filename.endsWith(".tif")) filename = filename.substring(0, filename.length() -3); // keep the dot
833 //Utils.log2("filename 2: " + filename);
835 // check if file ends with a tag of form ".id1234." where 1234 is p.getId()
836 final String tag = ".id" + p.getId() + ".";
837 if (!filename.endsWith(tag)) filename += tag.substring(1); // without the starting dot, since it has one already
838 // reappend extension
839 filename += "tif";
841 //Utils.log2("filename 3: " + filename);
843 path = getImageStorageFolder() + filename;
845 if (path.equals(p.getOriginalPath())) {
846 // Houston, we have a problem: a user reused a non-original image
847 File file = null;
848 int i = 1;
849 final int itag = path.lastIndexOf(tag);
850 do {
851 path = path.substring(0, itag) + "." + i + tag + "tif";
852 i++;
853 file = new File(path);
854 } while (file.exists());
857 //Utils.log2("path to use: " + path);
859 final String path2 = super.exportImage(p, imp, path, true);
861 //Utils.log2("path exported to: " + path2);
863 // update paths' hashtable
864 if (null != path2) {
865 updatePaths(p, path2, null != slice);
866 cacheAll(p, imp);
867 hs_unloadable.remove(p);
868 return path2;
869 } else {
870 Utils.log("WARNING could not save image at " + path);
871 // undo
872 updatePaths(p, starting_path, null != slice);
873 return null;
876 } catch (Exception e) {
877 IJError.print(e);
879 return null;
882 /** Associate patch with imp, and all slices as well if any. */
883 private void cacheAll(final Patch p, final ImagePlus imp) {
884 if (p.isStack()) {
885 for (Patch pa : p.getStackPatches()) {
886 cache(pa, imp);
888 } else {
889 cache(p, imp);
893 /** For the Patch and for any associated slices if the patch is part of a stack. */
894 private void updatePaths(final Patch patch, final String new_path, final boolean is_stack) {
895 synchronized (db_lock) {
896 try {
897 // ensure the old path is cached in the Patch, to get set as the original if there is no original.
898 String old_path = getAbsolutePath(patch);
899 if (is_stack) {
900 old_path = old_path.substring(0, old_path.lastIndexOf("-----#slice"));
901 for (Patch p : patch.getStackPatches()) {
902 long pid = p.getId();
903 String str = ht_paths.get(pid);
904 int isl = str.lastIndexOf("-----#slice=");
905 updatePatchPath(p, new_path + str.substring(isl));
907 } else {
908 Utils.log2("path to set: " + new_path);
909 Utils.log2("path before: " + ht_paths.get(patch.getId()));
910 updatePatchPath(patch, new_path);
911 Utils.log2("path after: " + ht_paths.get(patch.getId()));
913 mawts.updateImagePlusPath(old_path, new_path);
914 } catch (Throwable e) {
915 IJError.print(e);
920 /** With slice info appended at the end; only if it exists, otherwise null. */
921 public String getAbsolutePath(final Patch patch) {
922 synchronized (patch) {
923 String abs_path = patch.getCurrentPath();
924 if (null != abs_path) return abs_path;
925 // else, compute, set and return it:
926 String path = ht_paths.get(patch.getId());
927 if (null == path) return null;
928 // substract slice info if there
929 int i_sl = path.lastIndexOf("-----#slice=");
930 String slice = null;
931 if (-1 != i_sl) {
932 slice = path.substring(i_sl);
933 path = path.substring(0, i_sl);
935 path = getAbsolutePath(path);
936 if (null == path) {
937 Utils.log("Path for patch " + patch + " does not exist: " + path);
938 return null;
940 // Else assume that it exists.
941 // reappend slice info if existent
942 if (null != slice) path += slice;
943 // set it
944 patch.cacheCurrentPath(path);
945 return path;
949 /** Return an absolute path made from path: if it's already absolute, retursn itself; otherwise, the parent folder of all relative paths of this Loader is prepended. */
950 public String getAbsolutePath(String path) {
951 if (isRelativePath(path)) {
952 // path is relative: preprend the parent folder of the xml file
953 path = getParentFolder() + path;
954 if (!isURL(path) && !new File(path).exists()) {
955 return null;
958 return path;
961 public final String getImageFilePath(final Patch p) {
962 final String path = getAbsolutePath(p);
963 if (null == path) return null;
964 final int i = path.lastIndexOf("-----#slice");
965 return -1 == i ? path
966 : path.substring(0, i);
969 public static final boolean isURL(final String path) {
970 return null != path && 0 == path.indexOf("http://");
973 static public final Pattern ABS_PATH = Pattern.compile("^[a-zA-Z]*:/.*$|^/.*$|[a-zA-Z]:.*$");
975 public static final boolean isRelativePath(final String path) {
976 return ! ABS_PATH.matcher(path).matches();
979 /** All backslashes are converted to slashes to avoid havoc in MSWindows. */
980 public void addedPatchFrom(String path, final Patch patch) {
981 if (null == path) {
982 Utils.log("Null path for patch: " + patch);
983 return;
985 updatePatchPath(patch, path);
988 /** This method has the exclusivity in calling ht_paths.put, because it ensures the path won't have escape characters. */
989 private final void updatePatchPath(final Patch patch, String path) { // reversed order in purpose, relative to addedPatchFrom
990 // avoid W1nd0ws nightmares
991 path = path.replace('\\', '/'); // replacing with chars, in place
992 // remove double slashes that a user may have slipped in
993 final int start = isURL(path) ? 6 : (IJ.isWindows() ? 3 : 1);
994 while (-1 != path.indexOf("//", start)) {
995 // avoid the potential C:// of windows and the starting // of a samba network
996 path = path.substring(0, start) + path.substring(start).replace("//", "/");
998 // cache path as absolute
999 patch.cacheCurrentPath(isRelativePath(path) ? getParentFolder() + path : path);
1000 // if path is absolute, try to make it relative
1001 //Utils.log2("path was: " + path);
1002 path = makeRelativePath(path);
1003 // store
1004 ht_paths.put(patch.getId(), path);
1005 //Utils.log2("Updated patch path " + ht_paths.get(patch.getId()) + " for patch " + patch);
1008 /** Takes a String and returns a copy with the following conversions: / to -, space to _, and \ to -. */
1009 static public String asSafePath(final String name) {
1010 return name.trim().replace('/', '-').replace(' ', '_').replace('\\','-');
1013 /** 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. */
1014 @Override
1015 public String save(final Project project, XMLOptions options) {
1016 String result = null;
1017 if (null == project_file_path) {
1018 String xml_path = super.saveAs(project, null, options);
1019 if (null == xml_path) return null;
1020 else {
1021 this.project_file_path = xml_path;
1022 ControlWindow.updateTitle(project);
1023 result = this.project_file_path;
1025 } else {
1026 File fxml = new File(project_file_path);
1027 result = super.export(project, fxml, options);
1029 if (null != result) {
1030 Utils.logAll(Utils.now() + " Saved " + project);
1031 touched_mipmaps.clear();
1033 return result;
1036 /** The saveAs called from menus via saveTask. */
1037 @Override
1038 public String saveAs(Project project, XMLOptions options) {
1039 String path = super.saveAs(project, null, options);
1040 if (null != path) {
1041 // update the xml path to point to the new one
1042 this.project_file_path = path;
1043 Utils.log2("After saveAs, new xml path is: " + path);
1044 touched_mipmaps.clear();
1046 ControlWindow.updateTitle(project);
1047 Display.updateTitle(project);
1048 return path;
1051 /** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
1052 @Override
1053 public String saveAs(final String path, final XMLOptions options) {
1054 if (null == path) {
1055 Utils.log("Cannot save on null path.");
1056 return null;
1058 String path2 = path;
1059 String extension = ".xml";
1060 if (path2.endsWith(extension)) {} // all fine
1061 else if (path2.endsWith(".xml.gz")) extension = ".xml.gz";
1062 else {
1063 // neither matches, add the default ".xml"
1064 path2 += extension;
1067 File fxml = new File(path2);
1068 if (!fxml.canWrite()) {
1069 // write to storage folder instead
1070 String path3 = path2;
1071 path2 = getStorageFolder() + fxml.getName();
1072 Utils.logAll("WARNING can't write to " + path3 + "\n --> will write instead to " + path2);
1073 fxml = new File(path2);
1075 if (!options.overwriteXMLFile) {
1076 int i = 1;
1077 while (fxml.exists()) {
1078 String parent = fxml.getParent().replace('\\','/');
1079 if (!parent.endsWith("/")) parent += "/";
1080 String name = fxml.getName();
1081 name = name.substring(0, name.length() - 4);
1082 path2 = parent + name + "-" + i + extension;
1083 fxml = new File(path2);
1084 i++;
1087 Project project = Project.findProject(this);
1088 path2 = super.saveAs(project, path2, options);
1089 if (null != path2) {
1090 project_file_path = path2;
1091 Utils.logAll("After saveAs, new xml path is: " + path2);
1092 ControlWindow.updateTitle(project);
1093 touched_mipmaps.clear();
1095 return path2;
1098 /** Returns the stored path for the given Patch image, which may be relative and may contain slice information appended.*/
1099 public String getPath(final Patch patch) {
1100 return ht_paths.get(patch.getId());
1103 protected Map<Long,String> getPathsCopy() {
1104 synchronized (ht_paths) {
1105 return Collections.synchronizedMap(new HashMap<Long,String>(ht_paths));
1109 /** Try to make all paths in ht_paths be relative to the given xml_path.
1110 * This is intended for making all paths relative when saving to XML for the first time.
1111 * {@code dir_storage} and {@code dir_mipmaps} remain untouched--otherwise,
1112 * after a {@code saveAs}, images would not be found. */
1113 protected void makeAllPathsRelativeTo(final String xml_path, final Project project) {
1114 synchronized (db_lock) {
1115 try {
1116 for (final Map.Entry<Long,String> e : ht_paths.entrySet()) {
1117 e.setValue(FSLoader.makeRelativePath(xml_path, e.getValue()));
1119 for (final Stack st : project.getRootLayerSet().getAll(Stack.class)) {
1120 String path = st.getFilePath();
1121 if (!isRelativePath(path)) {
1122 String path2 = makeRelativePath(st.getFilePath());
1123 if (path.equals(path2)) continue; // could not be made relative
1124 else st.setFilePath(path2); // will also flush the cache, so use only if necessary
1127 } catch (Throwable t) {
1128 IJError.print(t);
1132 protected void restorePaths(final Map<Long,String> copy, final String mipmaps_folder, final String storage_folder) {
1133 synchronized (db_lock) {
1134 try {
1135 this.dir_mipmaps = mipmaps_folder;
1136 this.dir_storage = storage_folder;
1137 ht_paths.clear();
1138 ht_paths.putAll(copy);
1139 } catch (Throwable t) {
1140 IJError.print(t);
1145 /** 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. */
1146 public String makeRelativePath(String path) {
1147 return FSLoader.makeRelativePath(this.project_file_path, path);
1150 static private String makeRelativePath(final String project_file_path, String path) {
1151 if (null == project_file_path) {
1152 //unsaved project
1153 return path;
1155 if (null == path) {
1156 return null;
1158 // fix W1nd0ws paths
1159 path = path.replace('\\', '/'); // char-based, no parsing problems
1160 // remove slice tag
1161 String slice = null;
1162 int isl = path.lastIndexOf("-----#slice");
1163 if (-1 != isl) {
1164 slice = path.substring(isl);
1165 path = path.substring(0, isl);
1168 if (FSLoader.isRelativePath(path)) {
1169 // already relative
1170 if (-1 != isl) path += slice;
1171 return path;
1173 // the long and verbose way, to be cross-platform. Should work with URLs just the same.
1174 String xdir = new File(project_file_path).getParentFile().getAbsolutePath();
1175 if (IJ.isWindows()) {
1176 xdir = xdir.replace('\\', '/');
1177 path = path.replace('\\', '/');
1179 if (!xdir.endsWith("/")) xdir += "/";
1180 if (path.startsWith(xdir)) {
1181 path = path.substring(xdir.length());
1183 if (-1 != isl) path += slice;
1184 //Utils.log("made relative path: " + path);
1185 return path;
1188 /** Adds a "Save" and "Save as" menu items. */
1189 public void setupMenuItems(final JMenu menu, final Project project) {
1190 ActionListener listener = new ActionListener() {
1191 public void actionPerformed(ActionEvent ae) {
1192 saveTask(project, ae.getActionCommand());
1195 JMenuItem item;
1196 item = new JMenuItem("Save"); item.addActionListener(listener); menu.add(item);
1197 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true));
1198 item = new JMenuItem("Save as..."); item.addActionListener(listener); menu.add(item);
1199 final JMenu adv = new JMenu("Advanced");
1200 item = new JMenuItem("Save as... without coordinate transforms"); item.addActionListener(listener); adv.add(item);
1201 item = new JMenuItem("Delete stale files..."); item.addActionListener(listener); adv.add(item);
1202 menu.add(adv);
1203 menu.addSeparator();
1206 /** Returns the last Patch. */
1207 protected Patch importStackAsPatches(final Project project, final Layer first_layer, final double x, final double y, final ImagePlus imp_stack, final boolean as_copy, final String filepath) {
1208 Utils.log2("FSLoader.importStackAsPatches filepath=" + filepath);
1209 String target_dir = null;
1210 if (as_copy) {
1211 DirectoryChooser dc = new DirectoryChooser("Folder to save images");
1212 target_dir = dc.getDirectory();
1213 if (null == target_dir) return null; // user canceled dialog
1214 if (IJ.isWindows()) target_dir = target_dir.replace('\\', '/');
1215 if (target_dir.length() -1 != target_dir.lastIndexOf('/')) {
1216 target_dir += "/";
1220 // Double.MAX_VALUE is a flag to indicate "add centered"
1221 double pos_x = Double.MAX_VALUE != x ? x : first_layer.getLayerWidth()/2 - imp_stack.getWidth()/2;
1222 double pos_y = Double.MAX_VALUE != y ? y : first_layer.getLayerHeight()/2 - imp_stack.getHeight()/2;
1223 final double thickness = first_layer.getThickness();
1224 final String title = Utils.removeExtension(imp_stack.getTitle()).replace(' ', '_');
1225 Utils.showProgress(0);
1226 Patch previous_patch = null;
1227 final int n = imp_stack.getStackSize();
1229 final ImageStack stack = imp_stack.getStack();
1230 final boolean virtual = stack.isVirtual();
1231 final VirtualStack vs = virtual ? (VirtualStack)stack : null;
1233 for (int i=1; i<=n; i++) {
1234 Layer layer = first_layer;
1235 double z = first_layer.getZ() + (i-1) * thickness;
1236 if (i > 1) layer = first_layer.getParent().getLayer(z, thickness, true); // will create new layer if not found
1237 if (null == layer) {
1238 Utils.log("Display.importStack: could not create new layers.");
1239 return null;
1241 String patch_path = null;
1243 ImagePlus imp_patch_i = null;
1244 if (virtual) { // because we love inefficiency, every time all this is done again
1245 //VirtualStack vs = (VirtualStack)imp_stack.getStack();
1246 String vs_dir = vs.getDirectory().replace('\\', '/');
1247 if (!vs_dir.endsWith("/")) vs_dir += "/";
1248 String iname = vs.getFileName(i);
1249 patch_path = vs_dir + iname;
1250 Utils.log2("virtual stack: patch path is " + patch_path);
1251 releaseToFit(new File(patch_path).length() * 3);
1252 Utils.log2(i + " : " + patch_path);
1253 imp_patch_i = openImage(patch_path);
1254 } else {
1255 ImageProcessor ip = stack.getProcessor(i);
1256 if (as_copy) ip = ip.duplicate();
1257 imp_patch_i = new ImagePlus(title + "__slice=" + i, ip);
1260 String label = stack.getSliceLabel(i);
1261 if (null == label) label = "";
1262 Patch patch = null;
1263 if (as_copy) {
1264 patch_path = target_dir + cleanSlashes(imp_patch_i.getTitle()) + ".zip";
1265 ini.trakem2.io.ImageSaver.saveAsZip(imp_patch_i, patch_path);
1266 patch = new Patch(project, label + " " + title + " " + i, pos_x, pos_y, imp_patch_i);
1267 } else if (virtual) {
1268 patch = new Patch(project, label, pos_x, pos_y, imp_patch_i);
1269 } else {
1270 patch_path = filepath + "-----#slice=" + i;
1271 //Utils.log2("path is "+ patch_path);
1272 final AffineTransform atp = new AffineTransform();
1273 atp.translate(pos_x, pos_y);
1274 patch = new Patch(project, getNextId(), label + " " + title + " " + i, imp_stack.getWidth(), imp_stack.getHeight(), imp_stack.getWidth(), imp_stack.getHeight(), imp_stack.getType(), false, imp_stack.getProcessor().getMin(), imp_stack.getProcessor().getMax(), atp);
1275 patch.addToDatabase();
1276 //Utils.log2("type is " + imp_stack.getType());
1278 Utils.log2("B: " + i + " : " + patch_path);
1279 addedPatchFrom(patch_path, patch);
1280 if (!as_copy && !virtual) {
1281 if (virtual) cache(patch, imp_patch_i); // each slice separately
1282 else cache(patch, imp_stack); // uses the entire stack, shared among all Patch instances
1284 if (isMipMapsRegenerationEnabled()) regenerateMipMaps(patch); // submit for regeneration
1285 if (null != previous_patch) patch.link(previous_patch);
1286 layer.add(patch);
1287 previous_patch = patch;
1288 Utils.showProgress(i * (1.0 / n));
1290 Utils.showProgress(1.0);
1292 // update calibration
1293 // TODO
1295 // return the last patch
1296 return previous_patch;
1299 /** Replace forward slashes and backslashes with hyphens. */
1300 private final String cleanSlashes(final String s) {
1301 return s.replace('\\', '-').replace('/', '-');
1304 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1305 public void parseXMLOptions(final HashMap<String,String> ht_attributes) {
1306 // Adding some logic to support old projects which lack a storage folder and a mipmaps folder
1307 // and also to prevent errors such as those created when manualy tinkering with the XML file
1308 // or renaming directories, etc.
1309 String ob = ht_attributes.remove("storage_folder");
1310 if (null != ob) {
1311 String sf = ob.replace('\\', '/');
1312 if (isRelativePath(sf)) {
1313 sf = getParentFolder() + sf;
1315 if (isURL(sf)) {
1316 // can't be an URL
1317 Utils.log2("Can't have an URL as the path of a storage folder.");
1318 } else {
1319 File f = new File(sf);
1320 if (f.exists() && f.isDirectory()) {
1321 this.dir_storage = sf;
1322 } else {
1323 Utils.log2("storage_folder was not found or is invalid: " + ob);
1327 if (null == this.dir_storage) {
1328 // select the directory where the xml file lives.
1329 this.dir_storage = getParentFolder();
1330 if (null == this.dir_storage || isURL(this.dir_storage)) this.dir_storage = null;
1331 if (null == this.dir_storage && ControlWindow.isGUIEnabled()) {
1332 Utils.log2("Asking user for a storage folder in a dialog."); // tip for headless runners whose program gets "stuck"
1333 DirectoryChooser dc = new DirectoryChooser("REQUIRED: select a storage folder");
1334 this.dir_storage = dc.getDirectory();
1336 if (null == this.dir_storage) {
1337 IJ.showMessage("TrakEM2 requires a storage folder.\nTemporarily your home directory will be used.");
1338 this.dir_storage = System.getProperty("user.home");
1341 // fix
1342 if (null != this.dir_storage) {
1343 if (IJ.isWindows()) this.dir_storage = this.dir_storage.replace('\\', '/');
1344 if (!this.dir_storage.endsWith("/")) this.dir_storage += "/";
1346 Utils.log2("storage folder is " + this.dir_storage);
1348 ob = ht_attributes.remove("mipmaps_folder");
1349 if (null != ob) {
1350 String mf = ob.replace('\\', '/');
1351 if (isRelativePath(mf)) {
1352 mf = getParentFolder() + mf;
1354 if (isURL(mf)) {
1355 this.dir_mipmaps = mf;
1356 // TODO must disable input somehow, so that images are not edited.
1357 } else {
1358 File f = new File(mf);
1359 if (f.exists() && f.isDirectory()) {
1360 this.dir_mipmaps = mf;
1361 } else {
1362 Utils.log2("mipmaps_folder was not found or is invalid: " + ob);
1366 ob = ht_attributes.remove("mipmaps_regen");
1367 if (null != ob) {
1368 this.mipmaps_regen = Boolean.parseBoolean(ob);
1370 ob = ht_attributes.get("n_mipmap_threads");
1371 if (null != ob) {
1372 int n_threads = Math.max(1, Integer.parseInt(ob));
1373 FSLoader.restartMipMapThreads(n_threads);
1376 // parse the unuid before attempting to create any folders
1377 this.unuid = ht_attributes.remove("unuid");
1379 // Attempt to get an existing UNUId folder, for .xml files that share the same mipmaps folder
1380 if (ControlWindow.isGUIEnabled() && null == this.unuid) {
1381 obtainUNUIdFolder();
1384 if (null == this.dir_mipmaps) {
1385 // create a new one inside the dir_storage, which can't be null
1386 createMipMapsDir(dir_storage);
1387 if (null != this.dir_mipmaps && ControlWindow.isGUIEnabled() && null != IJ.getInstance()) {
1388 notifyMipMapsOutOfSynch();
1391 // fix
1392 if (null != this.dir_mipmaps && !this.dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
1393 Utils.log2("mipmaps folder is " + this.dir_mipmaps);
1395 if (null == unuid) {
1396 IJ.log("OLD VERSION DETECTED: your trakem2\nproject has been updated to the new format.\nPlease SAVE IT to avoid regenerating\ncached data when reopening it.");
1397 Utils.log2("Creating unuid for project " + this);
1398 this.unuid = createUNUId(dir_storage);
1399 fixStorageFolders();
1400 Utils.log2("Now mipmaps folder is " + this.dir_mipmaps);
1401 if (null != dir_masks) Utils.log2("Now masks folder is " + this.dir_masks);
1404 final String s_mipmaps_format = (String) ht_attributes.remove("mipmaps_format");
1405 if (null != s_mipmaps_format) {
1406 final int mipmaps_format = Integer.parseInt(s_mipmaps_format.trim());
1407 if (mipmaps_format >= 0 && mipmaps_format < MIPMAP_FORMATS.length) {
1408 Utils.log2("Set mipmap format to " + mipmaps_format);
1409 setMipMapFormat(mipmaps_format);
1414 private void notifyMipMapsOutOfSynch() {
1415 Utils.log2("'ok' dialog to explain that mipmaps may be in disagreement with the XML file."); // tip for headless runners whose program gets "stuck"
1416 Utils.showMessage("TrakEM2 detected a crash", "TrakEM2 detected a crash. Image mipmap files may be out of synch.\n\nIf you where editing images when the crash occurred,\nplease right-click and run 'Project - Regenerate all mipmaps'");
1419 /** Order the regeneration of all mipmaps for the Patch instances in @param patches, setting up a task that blocks input until all completed. */
1420 public Bureaucrat regenerateMipMaps(final Collection<? extends Displayable> patches) {
1421 return Bureaucrat.createAndStart(new Worker.Task("Regenerating mipmaps") { public void exec() {
1422 final List<Future<?>> fus = new ArrayList<Future<?>>();
1423 for (final Displayable d : patches) {
1424 if (d.getClass() != Patch.class) continue;
1425 fus.add(d.getProject().getLoader().regenerateMipMaps((Patch) d));
1427 // Wait until all done
1428 for (final Future<?> fu : fus) try {
1429 if (null != fu) fu.get(); // fu could be null if a task was not submitted because it's already being done or it failed in some way.
1430 } catch (Exception e) { IJError.print(e); }
1431 }}, Project.findProject(this));
1435 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1436 @Override
1437 public void insertXMLOptions(final StringBuilder sb_body, final String indent) {
1438 sb_body.append(indent).append("unuid=\"").append(unuid).append("\"\n");
1439 if (null != dir_mipmaps) sb_body.append(indent).append("mipmaps_folder=\"").append(makeRelativePath(dir_mipmaps)).append("\"\n");
1440 if (null != dir_storage) sb_body.append(indent).append("storage_folder=\"").append(makeRelativePath(dir_storage)).append("\"\n");
1441 sb_body.append(indent).append("mipmaps_format=\"").append(mipmaps_format).append("\"\n");
1444 /** Return the path to the folder containing the project XML file. */
1445 public final String getParentFolder() {
1446 return this.project_file_path.substring(0, this.project_file_path.lastIndexOf('/')+1);
1449 /* ************** MIPMAPS **********************/
1451 /** Returns the path to the directory hosting the file image pyramids. */
1452 public String getMipMapsFolder() {
1453 return dir_mipmaps;
1457 static private IndexColorModel thresh_cm = null;
1459 static private final IndexColorModel getThresholdLUT() {
1460 if (null == thresh_cm) {
1461 // An array of all black pixels (value 0) except at 255, which is white (value 255).
1462 final byte[] c = new byte[256];
1463 c[255] = (byte)255;
1464 thresh_cm = new IndexColorModel(8, 256, c, c, c);
1466 return thresh_cm;
1470 /** Returns the array of pixels, whose type depends on the bi.getType(); for example, for a BufferedImage.TYPE_BYTE_INDEXED, returns a byte[]. */
1471 static public final Object grabPixels(final BufferedImage bi) {
1472 final PixelGrabber pg = new PixelGrabber(bi, 0, 0, bi.getWidth(), bi.getHeight(), false);
1473 try {
1474 pg.grabPixels();
1475 return pg.getPixels();
1476 } catch (InterruptedException e) {
1477 IJError.print(e);
1479 return null;
1482 private final BufferedImage createCroppedAlpha(final BufferedImage alpha, final BufferedImage outside) {
1483 if (null == outside) return alpha;
1485 final int width = outside.getWidth();
1486 final int height = outside.getHeight();
1488 // Create an outside image, thresholded: only pixels of 255 remain as 255, the rest is set to 0.
1489 /* // DOESN'T work: creates a mask with "black" as 254 (???), and white 255 (correct).
1490 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, getThresholdLUT());
1491 thresholded.createGraphics().drawImage(outside, 0, 0, null);
1494 // So, instead: grab the pixels, fix them manually
1495 // The cast to byte[] works because "outside" and "alpha" are TYPE_BYTE_INDEXED.
1496 final byte[] o = (byte[])grabPixels(outside);
1497 if (null == o) return null;
1498 final byte[] a = null == alpha ? o : (byte[])grabPixels(alpha);
1500 // Set each non-255 pixel in outside to 0 in alpha:
1501 for (int i=0; i<o.length; i++) {
1502 if ( (o[i]&0xff) < 255) a[i] = 0;
1505 // Put the pixels back into an image:
1506 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
1507 thresholded.getRaster().setDataElements(0, 0, width, height, a);
1509 return thresholded;
1512 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1513 static final private byte[] gaussianBlurResizeInHalf(final FloatProcessorT2 source)
1515 new GaussianBlur().blurFloat( source, SIGMA_2, SIGMA_2, 0.01 );
1516 source.halfSizeInPlace();
1518 return (byte[])source.convertToByte(false).getPixels(); // no scaling
1521 /** Queue/unqueue for mipmap removal on shutdown without saving;
1522 * the {@param yes}, when true, makes the {@param p} be queued,
1523 * and when false, be removed from the queue. */
1524 public void queueForMipmapRemoval(final Patch p, boolean yes) {
1525 if (yes) touched_mipmaps.add(p);
1526 else touched_mipmaps.remove(p);
1529 /** Queue/unqueue for mipmap removal on shutdown without saving;
1530 * the {@param yes}, when true, makes the {@param p} be queued,
1531 * and when false, be removed from the queue. */
1532 public void tagForMipmapRemoval(final Patch p, final boolean yes) {
1533 if (yes) mipmaps_to_remove.add(p);
1534 else mipmaps_to_remove.remove(p);
1537 /** Given an image and its source file name (without directory prepended), generate
1538 * a pyramid of images until reaching an image not smaller than 32x32 pixels.<br />
1539 * Such images are stored as jpeg 85% quality in a folder named trakem2.mipmaps.<br />
1540 * The Patch id and the right extension will be appended to the filename in all cases.<br />
1541 * Any equally named files will be overwritten. */
1542 protected boolean generateMipMaps(final Patch patch) {
1543 Utils.log2("mipmaps for " + patch);
1544 final String path = getAbsolutePath(patch);
1545 if (null == path) {
1546 Utils.log("generateMipMaps: null path for Patch " + patch);
1547 cannot_regenerate.add(patch);
1548 return false;
1550 if (hs_unloadable.contains(patch)) {
1551 FilePathRepair.add(patch);
1552 return false;
1554 synchronized (gm_lock) {
1555 try {
1556 if (null == dir_mipmaps) createMipMapsDir(null);
1557 if (null == dir_mipmaps || isURL(dir_mipmaps)) return false;
1558 } catch (Exception e) {
1559 IJError.print(e);
1563 /** Record Patch as modified */
1564 touched_mipmaps.add(patch);
1566 /** Remove serialized features, if any */
1567 removeSerializedFeatures(patch);
1569 /** Remove serialized pointmatches, if any */
1570 removeSerializedPointMatches(patch);
1572 /** Alpha mask: setup to check if it was modified while regenerating. */
1573 final long alpha_mask_id = patch.getAlphaMaskId();
1575 final int resizing_mode = patch.getProject().getMipMapsMode();
1577 try {
1578 ImageProcessor ip;
1579 ByteProcessor alpha_mask = null;
1580 ByteProcessor outside_mask = null;
1581 int type = patch.getType();
1583 // Aggressive cache freeing
1584 releaseToFit(patch.getOWidth() * patch.getOHeight() * 4 + MIN_FREE_BYTES);
1586 // Obtain an image which may be coordinate-transformed, and an alpha mask.
1587 Patch.PatchImage pai = patch.createTransformedImage();
1588 if (null == pai || null == pai.target) {
1589 Utils.log("Can't regenerate mipmaps for patch " + patch);
1590 cannot_regenerate.add(patch);
1591 return false;
1593 ip = pai.target;
1594 alpha_mask = pai.mask; // can be null
1595 outside_mask = pai.outside; // can be null
1596 pai = null;
1598 // Old style:
1599 //final String filename = new StringBuilder(new File(path).getName()).append('.').append(patch.getId()).append(mExt).toString();
1600 // New style:
1601 final String filename = createMipMapRelPath(patch, mExt);
1603 int w = ip.getWidth();
1604 int h = ip.getHeight();
1606 // sigma = sqrt(2^level - 0.5^2)
1607 // where 0.5 is the estimated sigma for a full-scale image
1608 // which means sigma = 0.75 for the full-scale image (has level 0)
1609 // prepare a 0.75 sigma image from the original
1611 double min = patch.getMin(),
1612 max = patch.getMax();
1613 // Fix improper min,max values
1614 // (The -1,-1 are flags really for "not set")
1615 if (-1 == min && -1 == max) {
1616 switch (type) {
1617 case ImagePlus.COLOR_RGB:
1618 case ImagePlus.COLOR_256:
1619 case ImagePlus.GRAY8:
1620 patch.setMinAndMax(0, 255);
1621 break;
1622 // Find and flow through to default:
1623 case ImagePlus.GRAY16:
1624 ((ij.process.ShortProcessor)ip).findMinAndMax();
1625 patch.setMinAndMax(ip.getMin(), ip.getMax());
1626 break;
1627 case ImagePlus.GRAY32:
1628 ((FloatProcessor)ip).findMinAndMax();
1629 patch.setMinAndMax(ip.getMin(), ip.getMax());
1630 break;
1632 min = patch.getMin(); // may have changed
1633 max = patch.getMax();
1636 // Set for the level 0 image, which is a duplicate of the one in the cache in any case
1637 ip.setMinAndMax(min, max);
1640 // ImageJ no longer stretches the bytes for ByteProcessor with setMinAndmax
1641 if (ByteProcessor.class == ip.getClass()) {
1642 if (0 != min && 255 != max) {
1643 final byte[] b = (byte[]) ip.getPixels();
1644 final double scale = 255 / (max - min);
1645 for (int i=0; i<b.length; ++i) {
1646 final int val = b[i] & 0xff;
1647 if (val < min) b[i] = 0;
1648 else b[i] = (byte)Math.min(255, ((val - min) * scale));
1653 // Proper support for LUT images: treat them as RGB
1654 if (ip.isColorLut() || type == ImagePlus.COLOR_256) {
1655 ip = ip.convertToRGB();
1656 type = ImagePlus.COLOR_RGB;
1659 if (Loader.AREA_DOWNSAMPLING == resizing_mode) {
1660 long t0 = System.currentTimeMillis();
1661 final ImageBytes[] b = DownsamplerMipMaps.create(patch, type, ip, alpha_mask, outside_mask);
1662 long t1 = System.currentTimeMillis();
1663 for (int i=0; i<b.length; ++i) {
1664 mmio.save(getLevelDir(dir_mipmaps, i) + filename, b[i].c, b[i].width, b[i].height, 0.85f);
1666 long t2 = System.currentTimeMillis();
1667 System.out.println("MipMaps with area downsampling: creation took " + (t1 - t0) + "ms, saving took " + (t2 - t1) + "ms, total: " + (t2 - t0) + "ms\n");
1668 } else if (Loader.GAUSSIAN == resizing_mode) {
1669 if (ImagePlus.COLOR_RGB == type) {
1670 // TODO releaseToFit proper
1671 releaseToFit(w * h * 4 * 10);
1672 final ColorProcessor cp = (ColorProcessor)ip;
1673 final FloatProcessorT2 red = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(0, red);
1674 final FloatProcessorT2 green = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(1, green);
1675 final FloatProcessorT2 blue = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(2, blue);
1676 FloatProcessorT2 alpha;
1677 final FloatProcessorT2 outside;
1678 if (null != alpha_mask) {
1679 alpha = new FloatProcessorT2(alpha_mask);
1680 } else {
1681 alpha = null;
1683 if (null != outside_mask) {
1684 outside = new FloatProcessorT2(outside_mask);
1685 if ( null == alpha ) {
1686 alpha = outside;
1687 alpha_mask = outside_mask;
1689 } else {
1690 outside = null;
1693 final String target_dir0 = getLevelDir(dir_mipmaps, 0);
1695 if (Thread.currentThread().isInterrupted()) return false;
1697 // Generate level 0 first:
1698 // TODO Add alpha information into the int[] pixel array or make the image visible some other way
1699 if (!(null == alpha ? mmio.save(cp, target_dir0 + filename, 0.85f, false)
1700 : mmio.save(target_dir0 + filename, P.asRGBABytes((int[])cp.getPixels(), (byte[])alpha_mask.getPixels(), null == outside ? null : (byte[])outside_mask.getPixels()), w, h, 0.85f))) {
1701 Utils.log("Failed to save mipmap for COLOR_RGB, 'alpha = " + alpha + "', level = 0 for patch " + patch);
1702 cannot_regenerate.add(patch);
1703 } else {
1704 int k = 0; // the scale level. Proper scale is: 1 / pow(2, k)
1705 do {
1706 if (Thread.currentThread().isInterrupted()) return false;
1707 // 1 - Prepare values for the next scaled image
1708 k++;
1709 // 2 - Check that the target folder for the desired scale exists
1710 final String target_dir = getLevelDir(dir_mipmaps, k);
1711 if (null == target_dir) continue;
1712 // 3 - Blur the previous image to 0.75 sigma, and scale it
1713 final byte[] r = gaussianBlurResizeInHalf(red); // will resize 'red' FloatProcessor in place.
1714 final byte[] g = gaussianBlurResizeInHalf(green); // idem
1715 final byte[] b = gaussianBlurResizeInHalf(blue); // idem
1716 final byte[] a = null == alpha ? null : gaussianBlurResizeInHalf(alpha); // idem
1717 if ( null != outside ) {
1718 final byte[] o;
1719 if (alpha != outside)
1720 o = gaussianBlurResizeInHalf(outside); // idem
1721 else
1722 o = a;
1723 // Remove all not completely inside pixels from the alphamask
1724 // If there was no alpha mask, alpha is the outside itself
1725 for (int i=0; i<o.length; i++) {
1726 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;
1730 w = red.getWidth();
1731 h = red.getHeight();
1733 // 4 - Compose ColorProcessor
1734 if (null == alpha) {
1735 // 5 - Save as jpeg
1736 if (!mmio.save(target_dir + filename, new byte[][]{r, g, b}, w, h, 0.85f)) {
1737 Utils.log("Failed to save mipmap for COLOR_RGB, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
1738 cannot_regenerate.add(patch);
1739 break;
1741 } else {
1742 if (!mmio.save(target_dir + filename, new byte[][]{r, g, b, a}, w, h, 0.85f)) {
1743 Utils.log("Failed to save mipmap for COLOR_RGB, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
1744 cannot_regenerate.add(patch);
1745 break;
1748 } while (w >= 32 && h >= 32); // not smaller than 32x32
1750 } else {
1751 long t0 = System.currentTimeMillis();
1752 // Greyscale:
1753 releaseToFit(w * h * 4 * 10);
1755 if (Thread.currentThread().isInterrupted()) return false;
1757 final FloatProcessorT2 fp = new FloatProcessorT2((FloatProcessor) ip.convertToFloat());
1758 if (ImagePlus.GRAY8 == type) {
1759 // for 8-bit, the min,max has been applied when going to FloatProcessor
1760 fp.setMinMax(0, 255); // just set it
1761 } else {
1762 fp.setMinAndMax(patch.getMin(), patch.getMax());
1764 //fp.debugMinMax(patch.toString());
1766 FloatProcessorT2 alpha, outside;
1767 if (null != alpha_mask) {
1768 alpha = new FloatProcessorT2(alpha_mask);
1769 } else {
1770 alpha = null;
1772 if (null != outside_mask) {
1773 outside = new FloatProcessorT2(outside_mask);
1774 if (null == alpha) {
1775 alpha = outside;
1776 alpha_mask = outside_mask;
1778 } else {
1779 outside = null;
1782 int k = 0; // the scale level. Proper scale is: 1 / pow(2, k)
1783 do {
1784 if (Thread.currentThread().isInterrupted()) return false;
1786 if (0 != k) { // not doing so at the end because it would add one unnecessary blurring
1787 gaussianBlurResizeInHalf( fp );
1788 if (null != alpha) {
1789 gaussianBlurResizeInHalf( alpha );
1790 if (alpha != outside && outside != null) {
1791 gaussianBlurResizeInHalf( outside );
1796 w = fp.getWidth();
1797 h = fp.getHeight();
1799 // 1 - check that the target folder for the desired scale exists
1800 final String target_dir = getLevelDir(dir_mipmaps, k);
1801 if (null == target_dir) continue;
1803 if (null != alpha) {
1804 // 3 - save as jpeg with alpha
1805 // Remove all not completely inside pixels from the alpha mask
1806 // If there was no alpha mask, alpha is the outside itself
1807 if (!mmio.save(target_dir + filename, new byte[][]{fp.getScaledBytePixels(), P.merge(alpha.getBytePixels(), null == outside ? null : outside.getBytePixels())}, w, h, 0.85f)) {
1808 Utils.log("Failed to save mipmap for GRAY8, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
1809 cannot_regenerate.add(patch);
1810 break;
1812 } else {
1813 // 3 - save as 8-bit jpeg
1814 if (!mmio.save(target_dir + filename, new byte[][]{fp.getScaledBytePixels()}, w, h, 0.85f)) {
1815 Utils.log("Failed to save mipmap for GRAY8, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
1816 cannot_regenerate.add(patch);
1817 break;
1821 // 4 - prepare values for the next scaled image
1822 k++;
1823 } while (fp.getWidth() >= 32 && fp.getHeight() >= 32); // not smaller than 32x32
1825 long t1 = System.currentTimeMillis();
1826 System.out.println("MipMaps took " + (t1 - t0));
1828 } else {
1829 Utils.log("ERROR: unknown image resizing mode for mipmaps: " + resizing_mode);
1832 return true;
1833 } catch (Throwable e) {
1834 Utils.log("*** ERROR: Can't generate mipmaps for patch " + patch);
1835 IJError.print(e);
1836 cannot_regenerate.add(patch);
1837 return false;
1838 } finally {
1840 // flush any cached tiles
1841 flushMipMaps(patch.getId());
1843 // flush any cached layer screenshots
1844 if (null != patch.getLayer()) {
1845 try { patch.getLayer().getParent().removeFromOffscreens(patch.getLayer()); } catch (Exception e) { IJError.print(e); }
1848 // gets executed even when returning from the catch statement or within the try/catch block
1849 synchronized (gm_lock) {
1850 regenerating_mipmaps.remove(patch);
1853 // Has the alpha mask changed?
1854 if (patch.getAlphaMaskId() != alpha_mask_id) {
1855 Utils.log2("Alpha mask changed: resubmitting mipmap regeneration for " + patch);
1856 regenerateMipMaps(patch);
1862 /** Remove the file, if it exists, with serialized features for patch.
1863 * Returns true when no such file or on success; false otherwise. */
1864 public boolean removeSerializedFeatures(final Patch patch) {
1865 final File f = new File(new StringBuilder(getUNUIdFolder()).append("features.ser/").append(FSLoader.createIdPath(Long.toString(patch.getId()), "features", ".ser")).toString());
1866 if (f.exists()) {
1867 try {
1868 return f.delete();
1869 } catch (Exception e) {
1870 IJError.print(e);
1871 return false;
1873 } else return true;
1876 /** Remove the file, if it exists, with serialized point matches for patch.
1877 * Returns true when no such file or on success; false otherwise. */
1878 public boolean removeSerializedPointMatches(final Patch patch) {
1879 final String ser = new StringBuilder(getUNUIdFolder()).append("pointmatches.ser/").toString();
1880 final File fser = new File(ser);
1882 if (!fser.exists() || !fser.isDirectory()) return true;
1884 boolean success = true;
1885 final String sid = Long.toString(patch.getId());
1887 final ArrayList<String> removed_paths = new ArrayList<String>();
1889 // 1 - Remove all files with <p1.id>_<p2.id>:
1890 if (sid.length() < 2) {
1891 // Delete all files starting with sid + '_' and present directly under fser
1892 success = Utils.removePrefixedFiles(fser, sid + "_", removed_paths);
1893 } else {
1894 final String sid_ = sid + "_"; // minimal 2 length: a number and the underscore
1895 final int len = sid_.length();
1896 final StringBuilder dd = new StringBuilder();
1897 for (int i=1; i<=len; i++) {
1898 dd.append(sid_.charAt(i-1));
1899 if (0 == i % 2 && len != i) dd.append('/');
1901 final String med = dd.toString();
1902 final int last_slash = med.lastIndexOf('/');
1903 final File med_parent = new File(ser + med.substring(0, last_slash+1));
1904 // case of 12/34/_* ---> use prefix: "_"
1905 // case of 12/34/5_/* ---> use prefix: last number plus underscore, aka: med.substring(med.length()-2);
1906 success = Utils.removePrefixedFiles(med_parent,
1907 last_slash == med.length() -2 ? "_" : med.substring(med.length() -2),
1908 removed_paths);
1911 // 2 - For each removed path, find the complementary: <*>_<p1.id>
1912 for (String path : removed_paths) {
1913 if (IJ.isWindows()) path = path.replace('\\', '/');
1914 File f = new File(path);
1915 // Check that its a pointmatches file
1916 int idot = path.lastIndexOf(".pointmatches.ser");
1917 if (idot < 0) {
1918 Utils.log2("Not a pointmatches.ser file: can't process " + path);
1919 continue;
1922 // Find the root
1923 int ifolder = path.indexOf("pointmatches.ser/");
1924 if (ifolder < 0) {
1925 Utils.log2("Not in pointmatches.ser/ folder:" + path);
1926 continue;
1928 String dir = path.substring(0, ifolder + 17);
1930 // Cut the beginning and the end
1931 String name = path.substring(dir.length(), idot);
1932 Utils.log2("name: " + name);
1933 // Remove all path separators
1934 name = name.replaceAll("/", "");
1936 int iunderscore = name.indexOf('_');
1937 if (-1 == iunderscore) {
1938 Utils.log2("No underscore: can't process " + path);
1939 continue;
1941 name = FSLoader.createIdPath(new StringBuilder().append(name.substring(iunderscore+1)).append('_').append(name.substring(0, iunderscore)).toString(), "pointmatches", ".ser");
1943 f = new File(dir + name);
1944 if (f.exists()) {
1945 if (!f.delete()) {
1946 Utils.log2("Could not delete " + f.getAbsolutePath());
1947 success = false;
1948 } else {
1949 Utils.log2("Deleted pointmatches file " + name);
1950 // Now remove its parent directories within pointmatches.ser/ directory, if they are empty
1951 int islash = name.lastIndexOf('/');
1952 String dirname = name;
1953 while (islash > -1) {
1954 dirname = dirname.substring(0, islash);
1955 if (!Utils.removeFile(new File(dir + dirname))) {
1956 // directory not empty
1957 break;
1959 islash = dirname.lastIndexOf('/');
1962 } else {
1963 Utils.log2("File does not exist: " + dir + name);
1967 return success;
1970 /** 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.
1972 * @param al : the list of Patch instances to generate mipmaps for.
1973 * @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.)
1974 * */
1975 public Bureaucrat generateMipMaps(final Collection<Displayable> patches, final boolean overwrite) {
1976 if (null == patches || 0 == patches.size()) return null;
1977 if (null == dir_mipmaps) createMipMapsDir(null);
1978 if (isURL(dir_mipmaps)) {
1979 Utils.log("Mipmaps folder is an URL, can't save files into it.");
1980 return null;
1982 return Bureaucrat.createAndStart(new Worker.Task("Generating MipMaps") {
1983 public void exec() {
1984 this.setAsBackground(true);
1985 Utils.log2("starting mipmap generation ..");
1986 try {
1987 final ArrayList<Future<?>> fus = new ArrayList<Future<?>>();
1988 for (final Displayable displ : patches) {
1989 if (displ.getClass() != Patch.class) continue;
1990 Patch pa = (Patch)displ;
1991 boolean ow = overwrite;
1992 if (!overwrite) {
1993 // check if all the files exist. If one doesn't, then overwrite all anyway
1994 int w = (int)pa.getWidth();
1995 int h = (int)pa.getHeight();
1996 int level = 0;
1997 final String filename = new File(getAbsolutePath(pa)).getName() + "." + pa.getId() + mExt;
1998 do {
1999 w /= 2;
2000 h /= 2;
2001 level++;
2002 if (!new File(dir_mipmaps + level + "/" + filename).exists()) {
2003 ow = true;
2004 break;
2006 } while (w >= 32 && h >= 32);
2008 if (!ow) continue;
2009 fus.add(regenerateMipMaps(pa));
2012 Utils.wait(fus);
2014 } catch (Exception e) {
2015 IJError.print(e);
2018 }, ((Displayable)patches.iterator().next()).getProject());
2021 static private final Object FSLOCK = new Object();
2023 private final String getLevelDir(final String dir_mipmaps, final int level) {
2024 // synch, so that multithreaded generateMipMaps won't collide trying to create dirs
2025 synchronized (FSLOCK) {
2026 final String path = new StringBuilder(dir_mipmaps).append(level).append('/').toString();
2027 if (isURL(dir_mipmaps)) {
2028 return path;
2030 final File file = new File(path);
2031 if (file.exists() && file.isDirectory()) {
2032 return path;
2034 // else, create it
2035 try {
2036 file.mkdir();
2037 return path;
2038 } catch (Exception e) {
2039 IJError.print(e);
2040 return null;
2045 /** Returns the near-unique folder for the project hosted by this FSLoader. */
2046 public String getUNUIdFolder() {
2047 return new StringBuilder(getStorageFolder()).append("trakem2.").append(unuid).append('/').toString();
2050 /** Return the unuid_dir or null if none valid selected. */
2051 private String obtainUNUIdFolder() {
2052 YesNoCancelDialog yn = ControlWindow.makeYesNoCancelDialog("Old .xml version!", "The loaded XML file does not contain an UNUId. Select a shared UNUId folder?\nShould look similar to: trakem2.12345678.12345678.12345678");
2053 if (!yn.yesPressed()) return null;
2054 DirectoryChooser dc = new DirectoryChooser("Select UNUId folder");
2055 String unuid_dir = dc.getDirectory();
2056 String unuid_dir_name = new File(unuid_dir).getName();
2057 Utils.log2("Selected UNUId folder: " + unuid_dir + "\n with name: " + unuid_dir_name);
2058 if (null != unuid_dir) {
2059 if (IJ.isWindows()) unuid_dir = unuid_dir.replace('\\', '/');
2060 if ( ! unuid_dir_name.startsWith("trakem2.")) {
2061 Utils.logAll("Invalid UNUId folder: must start with \"trakem2.\". Try again or cancel.");
2062 return obtainUNUIdFolder();
2063 } else {
2064 String[] nums = unuid_dir_name.split("\\.");
2065 if (nums.length != 4) {
2066 Utils.logAll("Invalid UNUId folder: needs trakem + 3 number blocks. Try again or cancel.");
2067 return obtainUNUIdFolder();
2069 for (int i=1; i<nums.length; i++) {
2070 try {
2071 Long.parseLong(nums[i]);
2072 } catch (NumberFormatException nfe) {
2073 Utils.logAll("Invalid UNUId folder: at least one block is not a number. Try again or cancel.");
2074 return obtainUNUIdFolder();
2077 // ok, aceptamos pulpo
2078 String unuid = unuid_dir_name.substring(8); // remove prefix "trakem2."
2079 if (unuid.endsWith("/")) unuid = unuid.substring(0, unuid.length() -1);
2080 this.unuid = unuid;
2082 if (!unuid_dir.endsWith("/")) unuid_dir += "/";
2084 String dir_storage = new File(unuid_dir).getParent().replace('\\', '/');
2085 if (!dir_storage.endsWith("/")) dir_storage += "/";
2086 this.dir_storage = dir_storage;
2088 this.dir_mipmaps = unuid_dir + "trakem2.mipmaps/";
2090 return unuid_dir;
2093 return null;
2096 /** If parent path is null, it's asked for.*/
2097 private boolean createMipMapsDir(String parent_path) {
2098 if (null == this.unuid) this.unuid = createUNUId(parent_path);
2099 if (null == parent_path) {
2100 // try to create it in the same directory where the XML file is
2101 if (null != dir_storage) {
2102 File f = new File(getUNUIdFolder() + "/trakem2.mipmaps");
2103 if (!f.exists()) {
2104 try {
2105 if (f.mkdir()) {
2106 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
2107 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
2108 return true;
2110 } catch (Exception e) {}
2111 } else if (f.isDirectory()) {
2112 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
2113 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
2114 return true;
2116 // else can't use it
2118 // else, ask for a new folder
2119 final DirectoryChooser dc = new DirectoryChooser("Select MipMaps parent directory");
2120 parent_path = dc.getDirectory();
2121 if (null == parent_path) return false;
2122 if (IJ.isWindows()) parent_path = parent_path.replace('\\', '/');
2123 if (!parent_path.endsWith("/")) parent_path += "/";
2125 // examine parent path
2126 final File file = new File(parent_path);
2127 if (file.exists()) {
2128 if (file.isDirectory()) {
2129 // all OK
2130 this.dir_mipmaps = parent_path + "trakem2." + unuid + "/trakem2.mipmaps/";
2131 try {
2132 File f = new File(this.dir_mipmaps);
2133 f.mkdirs();
2134 if (!f.exists()) {
2135 Utils.log("Could not create trakem2.mipmaps!");
2136 return false;
2138 } catch (Exception e) {
2139 IJError.print(e);
2140 return false;
2142 } else {
2143 Utils.showMessage("Selected parent path is not a directory. Please choose another one.");
2144 return createMipMapsDir(null);
2146 } else {
2147 Utils.showMessage("Parent path does not exist. Please select a new one.");
2148 return createMipMapsDir(null);
2150 return true;
2153 /** Remove all mipmap images from the cache, and optionally set the dir_mipmaps to null. */
2154 public void flushMipMaps(boolean forget_dir_mipmaps) {
2155 if (null == dir_mipmaps) return;
2156 synchronized (db_lock) {
2157 try {
2158 if (forget_dir_mipmaps) this.dir_mipmaps = null;
2159 mawts.removeAndFlushAll();
2160 } catch (Throwable t) {
2161 handleCacheError(t);
2166 /** Remove from the cache all images of level larger than zero corresponding to the given patch id. */
2167 public void flushMipMaps(final long id) {
2168 if (null == dir_mipmaps) return;
2169 synchronized (db_lock) {
2170 try {
2171 mawts.removeAndFlushPyramid(id);
2172 } catch (Throwable t) {
2173 handleCacheError(t);
2178 /** Gets data from the Patch and queues a new task to do the file removal in a separate task manager thread. */
2179 public Future<Boolean> removeMipMaps(final Patch p) {
2180 return removeMipMaps(p, mExt);
2183 private Future<Boolean> removeMipMaps(final Patch p, final String extension) {
2184 if (null == dir_mipmaps) return null;
2185 // cache values before they are changed:
2186 final int width = (int)p.getWidth();
2187 final int height = (int)p.getHeight();
2188 return remover.submit(new Callable<Boolean>() {
2189 public Boolean call() {
2190 try {
2191 final String path = getAbsolutePath(p);
2192 if (null == path) {
2193 // missing file
2194 Utils.log2("Remover: null path for Patch " + p);
2195 return false;
2197 removeMipMaps(createIdPath(Long.toString(p.getId()), new File(path).getName(), extension), width, height);
2198 flushMipMaps(p.getId());
2199 return true;
2200 } catch (Exception e) {
2201 IJError.print(e);
2203 return false;
2208 private void removeMipMaps(final String filename, final int width, final int height) {
2209 int w = width;
2210 int h = height;
2211 int k = 0; // the level
2212 do {
2213 final File f = new File(new StringBuilder(dir_mipmaps).append(k).append('/').append(filename).toString());
2214 if (f.exists()) {
2215 try {
2216 if (!f.delete()) {
2217 Utils.log2("Could not remove file " + f.getAbsolutePath());
2219 } catch (Exception e) {
2220 IJError.print(e);
2223 w /= 2;
2224 h /= 2;
2225 k++;
2226 } while (w >= 32 && h >= 32); // not smaller than 32x32
2229 @Override
2230 public boolean usesMipMapsFolder() {
2231 return null != dir_mipmaps;
2234 /** Return the closest level to @param level that exists as a file.
2235 * If no valid path is found for the patch, returns ERROR_PATH_NOT_FOUND.
2237 @Override
2238 public int getClosestMipMapLevel(final Patch patch, int level, final int max_level) {
2239 if (null == dir_mipmaps) return 0;
2240 try {
2241 final String path = getAbsolutePath(patch);
2242 if (null == path) return ERROR_PATH_NOT_FOUND;
2243 final String filename = new File(path).getName() + mExt;
2244 if (isURL(dir_mipmaps)) {
2245 if (level <= 0) return 0;
2246 // choose the smallest dimension
2247 // find max level that keeps dim over 32 pixels
2248 if (level > max_level) return max_level;
2249 return level;
2250 } else {
2251 do {
2252 final File f = new File(new StringBuilder(dir_mipmaps).append(level).append('/').append(filename).toString());
2253 if (f.exists()) {
2254 return level;
2256 // try the next level
2257 level--;
2258 } while (level >= 0);
2260 } catch (Exception e) {
2261 IJError.print(e);
2263 return 0;
2266 /** A temporary list of Patch instances for which a pyramid is being generated.
2267 * Access is synchronized by gm_lock. */
2268 final private Map<Patch,Future<Boolean>> regenerating_mipmaps = new HashMap<Patch,Future<Boolean>>();
2270 /** A lock for the generation of mipmaps. */
2271 final private Object gm_lock = new Object();
2273 /** Checks if the mipmap file for the Patch and closest upper level to the desired magnification exists. */
2274 public boolean checkMipMapFileExists(final Patch p, final double magnification) {
2275 if (null == dir_mipmaps) return false;
2276 final int level = getMipMapLevel(magnification, maxDim(p));
2277 if (isURL(dir_mipmaps)) return true; // just assume that it does
2278 if (new File(dir_mipmaps + level + "/" + new File(getAbsolutePath(p)).getName() + "." + p.getId() + mExt).exists()) return true;
2279 return false;
2282 final Set<Patch> cannot_regenerate = Collections.synchronizedSet(new HashSet<Patch>());
2284 /** Loads the file containing the scaled image corresponding to the given level
2285 * (or the maximum possible level, if too large)
2286 * and returns it as an awt.Image, or null if not found.
2287 * Will also regenerate the mipmaps, i.e. recreate the pre-scaled jpeg images if they are missing.
2288 * Does NOT release memory, avoiding locking on the db_lock. */
2289 protected MipMapImage fetchMipMapAWT(final Patch patch, final int level, final long n_bytes) {
2290 return fetchMipMapAWT(patch, level, n_bytes, 0);
2293 /** Does the actual fetching of the file. Returns null if the file does not exist.
2294 * Does NOT pre-release memory from the cache;
2295 * call releaseToFit to do that. */
2296 public final MipMapImage fetchMipMap(final Patch patch, int level, final long n_bytes) {
2297 final int max_level = getHighestMipMapLevel(patch);
2298 if ( level > max_level ) level = max_level;
2299 final double scale = Math.pow( 2.0, level );
2301 final String filename = getInternalFileName(patch);
2302 if (null == filename) {
2303 Utils.log2("null internal filename!");
2304 return null;
2307 // New style:
2308 final String path = new StringBuilder(dir_mipmaps).append( level ).append('/').append(createIdPath(Long.toString(patch.getId()), filename, mExt)).toString();
2310 //releaseToFit(n_bytes * 8); // eight times, for the jpeg decoder alloc/dealloc at least 2 copies, and with alpha even one more
2311 // TODO the x8 is overly exaggerated
2313 if ( patch.hasAlphaChannel() ) {
2314 final Image img = mmio.open( path );
2315 return img == null ? null : new MipMapImage( img, scale, scale );
2316 } else if ( patch.paintsWithFalseColor() ) {
2317 // AKA Patch has a LUT or is LUT image like a GIF
2318 final Image img = mmio.open( path );
2319 return img == null ? null : new MipMapImage( img, scale, scale ); // considers c_alphas
2320 } else {
2321 final Image img;
2322 switch (patch.getType()) {
2323 case ImagePlus.GRAY16:
2324 case ImagePlus.GRAY8:
2325 case ImagePlus.GRAY32:
2326 img = mmio.openGrey( path ); // ImageSaver.openGreyJpeg(path);
2327 return img == null ? null : new MipMapImage( img, scale, scale );
2328 default:
2329 // For color images: (considers URL as well)
2330 img = mmio.open( path );
2331 return img == null ? null : new MipMapImage( img, scale, scale ); // considers c_alphas
2336 /** Will NOT free memory. */
2337 private final MipMapImage fetchMipMapAWT(final Patch patch, final int level, final long n_bytes, final int retries) {
2338 if (null == dir_mipmaps) {
2339 Utils.log2("null dir_mipmaps");
2340 return null;
2342 while (retries < MAX_RETRIES) {
2343 try {
2344 // TODO should wait if the file is currently being generated
2346 final MipMapImage mipMap = fetchMipMap(patch, level, n_bytes);
2347 if (null != mipMap) return mipMap;
2349 // if we got so far ... try to regenerate the mipmaps
2350 if (!mipmaps_regen) {
2351 return null;
2354 // check that REALLY the file doesn't exist.
2355 if (cannot_regenerate.contains(patch)) {
2356 Utils.log("Cannot regenerate mipmaps for patch " + patch);
2357 return null;
2360 //Utils.log2("getMipMapAwt: imp is " + imp + " for path " + dir_mipmaps + level + "/" + new File(getAbsolutePath(patch)).getName() + "." + patch.getId() + mExt);
2362 // Regenerate in the case of not asking for an image under 32x32
2363 double scale = 1 / Math.pow(2, level);
2364 if (level >= 0 && patch.getWidth() * scale >= 32 && patch.getHeight() * scale >= 32 && isMipMapsRegenerationEnabled()) {
2365 // regenerate in a separate thread
2366 regenerateMipMaps( patch );
2367 return new MipMapImage( REGENERATING, patch.getWidth() / REGENERATING.getWidth(), patch.getHeight() / REGENERATING.getHeight() );
2369 } catch (OutOfMemoryError oome) {
2370 Utils.log2("fetchMipMapAWT: recovering from OutOfMemoryError");
2371 recoverOOME();
2372 Thread.yield();
2373 // Retry:
2374 return fetchMipMapAWT(patch, level, n_bytes, retries + 1);
2375 } catch (Throwable t) {
2376 IJError.print(t);
2379 return null;
2382 static private AtomicInteger n_regenerating = new AtomicInteger(0);
2383 static private ExecutorService regenerator = null;
2384 static private ExecutorService remover = null;
2385 static public ExecutorService repainter = null;
2386 static private int nStaticServiceThreads = nStaticServiceThreads();
2387 static public ScheduledExecutorService autosaver = null;
2389 static private final class DONE implements Future<Boolean>
2391 @Override
2392 public boolean cancel(boolean mayInterruptIfRunning) {
2393 return true;
2395 @Override
2396 public Boolean get() throws InterruptedException, ExecutionException {
2397 return true;
2399 @Override
2400 public Boolean get(long timeout, TimeUnit unit)
2401 throws InterruptedException, ExecutionException,
2402 TimeoutException {
2403 return true;
2405 @Override
2406 public boolean isCancelled() {
2407 return false;
2409 @Override
2410 public boolean isDone() {
2411 return true;
2415 /** Queue the regeneration of mipmaps for the Patch; returns immediately, having submitted the job to an executor queue;
2416 * returns a Future if the task was submitted, null if not. */
2417 @Override
2418 public final Future<Boolean> regenerateMipMaps(final Patch patch) {
2420 if (!isMipMapsRegenerationEnabled()) {
2421 // If not enabled, the cache must be flushed
2422 flushMipMaps(patch.getId());
2423 return new DONE();
2426 synchronized (gm_lock) {
2427 try {
2428 Future<Boolean> fu = regenerating_mipmaps.get(patch);
2429 if (null != fu) return fu;
2431 // else, start it
2433 n_regenerating.incrementAndGet();
2434 Utils.log2("SUBMITTING to regen " + patch);
2435 Utils.showStatus(new StringBuilder("Regenerating mipmaps (").append(n_regenerating.get()).append(" to go)").toString());
2437 // Eliminate existing mipmaps, if any, in a separate thread:
2438 //Utils.log2("calling removeMipMaps from regenerateMipMaps");
2439 final Future<Boolean> removing = removeMipMaps(patch);
2441 fu = regenerator.submit(new Callable<Boolean>() {
2442 public Boolean call() {
2443 boolean b = false;
2444 try {
2445 // synchronize with the removal:
2446 if (null != removing) removing.get();
2447 Utils.showStatus(new StringBuilder("Regenerating mipmaps (").append(n_regenerating.get()).append(" to go)").toString());
2448 b = generateMipMaps(patch); // will remove the Future from the regenerating_mipmaps table, under proper gm_lock synchronization
2449 Display.repaint(patch.getLayer());
2450 Display.updatePanel(patch.getLayer(), patch);
2451 Utils.showStatus("");
2452 } catch (Exception e) {
2453 IJError.print(e);
2455 n_regenerating.decrementAndGet();
2456 return b;
2460 regenerating_mipmaps.put(patch, fu);
2462 return fu;
2464 } catch (Exception e) {
2465 IJError.print(e);
2466 return null;
2471 /** 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.
2472 public long estimateImageFileSize(final Patch p, final int level) {
2473 if (level > 0) {
2474 // jpeg image to be loaded:
2475 final double scale = 1 / Math.pow(2, level);
2476 return (long)(p.getWidth() * scale * p.getHeight() * scale * 5 + 1024);
2478 long size = (long)(p.getWidth() * p.getHeight());
2479 int bytes_per_pixel = 1;
2480 final int type = p.getType();
2481 switch (type) {
2482 case ImagePlus.GRAY32:
2483 bytes_per_pixel = 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
2484 break;
2485 case ImagePlus.GRAY16:
2486 bytes_per_pixel = 3; // 2 for the ShortProcessor, and 1 for the pixels8
2487 case ImagePlus.COLOR_RGB:
2488 bytes_per_pixel = 4;
2489 break;
2490 case ImagePlus.GRAY8:
2491 case ImagePlus.COLOR_256:
2492 bytes_per_pixel = 1;
2493 // check jpeg, which can only encode RGB (taken care of above) and 8-bit and 8-bit color images:
2494 String path = ht_paths.get(p.getId());
2495 if (null != path && path.endsWith(mExt)) bytes_per_pixel = 5; //4 for the int[] and 1 for the byte[]
2496 break;
2497 default:
2498 bytes_per_pixel = 5; // conservative
2499 break;
2502 return size * bytes_per_pixel + 1024;
2505 public String makeProjectName() {
2506 if (null == project_file_path || 0 == project_file_path.length()) return super.makeProjectName();
2507 final String name = new File(project_file_path).getName();
2508 final int i_dot = name.lastIndexOf('.');
2509 if (-1 == i_dot) return name;
2510 if (0 == i_dot) return super.makeProjectName();
2511 return name.substring(0, i_dot);
2515 /** Returns the path where the imp is saved to: the storage folder plus a name. */
2516 public String handlePathlessImage(final ImagePlus imp) {
2517 FileInfo fi = imp.getOriginalFileInfo();
2518 if (null == fi) fi = imp.getFileInfo();
2519 if (null == fi.fileName || fi.fileName.equals("")) {
2520 fi.fileName = "img_" + System.currentTimeMillis() + ".tif";
2522 if (!fi.fileName.endsWith(".tif")) fi.fileName += ".tif";
2523 fi.directory = dir_storage;
2524 if (imp.getNSlices() > 1) {
2525 new FileSaver(imp).saveAsTiffStack(dir_storage + fi.fileName);
2526 } else {
2527 new FileSaver(imp).saveAsTiff(dir_storage + fi.fileName);
2529 Utils.log2("Saved a copy into the storage folder:\n" + dir_storage + fi.fileName);
2530 return dir_storage + fi.fileName;
2533 /** Convert old-style storage folders to new style. */
2534 public boolean fixStorageFolders() {
2535 try {
2536 // 1 - Create folder unuid_folder at storage_folder + unuid
2537 if (null == this.unuid) {
2538 Utils.log2("No unuid for project!");
2539 return false;
2541 // the trakem2.<unuid> folder that will now contain trakem2.mipmaps, trakem2.masks, etc.
2542 final String unuid_folder = getUNUIdFolder();
2543 File fdir = new File(unuid_folder);
2544 if (!fdir.exists()) {
2545 if (!fdir.mkdir()) {
2546 Utils.log2("Could not create folder " + unuid_folder);
2547 return false;
2550 // 2 - Create trakem2.mipmaps inside unuid folder
2551 final String new_dir_mipmaps = unuid_folder + "trakem2.mipmaps/";
2552 fdir = new File(new_dir_mipmaps);
2553 if (!fdir.mkdir()) {
2554 Utils.log2("Could not create folder " + new_dir_mipmaps);
2555 return false;
2557 // 3 - Reorganize current mipmaps folder to folders with following convention: <level>/dd/dd/d.jpg where ddddd is Patch.id=12345 12/34/5.jpg etc.
2558 final String dir_mipmaps = getMipMapsFolder();
2559 for (final String name : new File(dir_mipmaps).list()) {
2560 final String level_dir = new StringBuilder(dir_mipmaps).append(name).append('/').toString();
2561 final File f = new File(level_dir);
2562 if (!f.isDirectory() || f.isHidden()) continue;
2563 for (final String mm : f.list()) {
2564 if (!mm.endsWith(mExt)) continue;
2565 // parse the mipmap file: filename + '.' + id + '.jpg'
2566 int last_dot = mm.lastIndexOf('.');
2567 if (-1 == last_dot) continue;
2568 int prev_last_dot = mm.lastIndexOf('.', last_dot -1);
2569 String id = mm.substring(prev_last_dot+1, last_dot);
2570 String filename = mm.substring(0, prev_last_dot);
2571 File oldf = new File(level_dir + mm);
2572 File newf = new File(new StringBuilder(new_dir_mipmaps).append(name).append('/').append(createIdPath(id, filename, mExt)).toString());
2573 File fd = newf.getParentFile();
2574 fd.mkdirs();
2575 if (!fd.exists()) {
2576 Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
2577 continue;
2579 if (!oldf.renameTo(newf)) {
2580 Utils.log2("Could not move mipmap file " + oldf.getAbsolutePath() + " to " + newf.getAbsolutePath());
2581 continue;
2585 // Set it!
2586 this.dir_mipmaps = new_dir_mipmaps;
2588 // Remove old empty dirs:
2589 Utils.removeFile(new File(dir_mipmaps));
2591 // 4 - same for alpha folder and features folder.
2592 final String masks_folder = getStorageFolder() + "trakem2.masks/";
2593 File fmasks = new File(masks_folder);
2594 this.dir_masks = null;
2595 if (fmasks.exists()) {
2596 final String new_dir_masks = unuid_folder + "trakem2.masks/";
2597 final File[] fmask_files = fmasks.listFiles();
2598 if (null != fmask_files) { // can be null if there are no files inside fmask directory
2599 for (final File fmask : fmask_files) {
2600 final String name = fmask.getName();
2601 if (!name.endsWith(".zip")) continue;
2602 int last_dot = name.lastIndexOf('.');
2603 if (-1 == last_dot) continue;
2604 int prev_last_dot = name.lastIndexOf('.', last_dot -1);
2605 String id = name.substring(prev_last_dot+1, last_dot);
2606 String filename = name.substring(0, prev_last_dot);
2607 File newf = new File(new_dir_masks + createIdPath(id, filename, ".zip"));
2608 File fd = newf.getParentFile();
2609 fd.mkdirs();
2610 if (!fd.exists()) {
2611 Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
2612 continue;
2614 if (!fmask.renameTo(newf)) {
2615 Utils.log2("Could not move mask file " + fmask.getAbsolutePath() + " to " + newf.getAbsolutePath());
2616 continue;
2620 // Set it!
2621 this.dir_masks = new_dir_masks;
2623 // remove old empty:
2624 Utils.removeFile(fmasks);
2627 // TODO should save the .xml file, so the unuid and the new storage folders are set in there!
2629 return true;
2630 } catch (Exception e) {
2631 IJError.print(e);
2633 return false;
2636 /** For Patch id=12345 creates 12/34/5.${filename}.jpg */
2637 static public final String createMipMapRelPath(final Patch p, final String ext) {
2638 return createIdPath(Long.toString(p.getId()), new File(p.getCurrentPath()).getName(), ext);
2641 /** For sid=12345 creates 12/34/5.${filename}.jpg
2642 * Will be fine with other filename-valid chars in sid. */
2643 static public final String createIdPath(final String sid, final String filename, final String ext) {
2644 final StringBuilder sf = new StringBuilder(((sid.length() * 3) / 2) + 1);
2645 final int len = sid.length();
2646 for (int i=1; i<=len; i++) {
2647 sf.append(sid.charAt(i-1));
2648 if (0 == i % 2 && len != i) sf.append('/');
2650 return sf.append('.').append(filename).append(ext).toString();
2653 public String getUNUId() {
2654 return unuid;
2658 /** Waits until a proper image of the desired size or larger can be returned, which is never the Loader.REGENERATING image.
2659 * If no image can be loaded, returns Loader.NOT_FOUND.
2660 * If the Patch is undergoing mipmap regeneration, it waits until done.
2662 @Override
2663 public MipMapImage fetchDataImage( final Patch p, final double mag) {
2664 Future<Boolean> fu = null;
2665 MipMapImage mipMap = null;
2666 synchronized (gm_lock) {
2667 fu = regenerating_mipmaps.get(p);
2669 if (null == fu) {
2670 // Patch is currently not under regeneration
2671 mipMap = fetchImage( p, mag );
2672 // If the patch mipmaps didn't exist,
2673 // the call to fetchImage will trigger mipmap regeneration
2674 // and img will be now Loader.REGENERATING
2675 if (Loader.REGENERATING != mipMap.image ) {
2676 return mipMap;
2677 } else {
2678 synchronized (gm_lock) {
2679 fu = regenerating_mipmaps.get(p);
2683 if (null != fu) {
2684 try {
2685 if ( ! fu.get()) {
2686 Utils.log("Loader.fetchDataImage: could not regenerate mipmaps and get an image for patch " + p);
2687 return new MipMapImage( NOT_FOUND, p.getWidth() / NOT_FOUND.getWidth(), p.getHeight() / NOT_FOUND.getHeight() );
2689 // Now the image should be good:
2690 mipMap = fetchImage(p, mag);
2692 // Check in any case:
2693 if (Loader.isSignalImage(mipMap.image)) {
2694 // Attempt to create from scratch
2695 return new MipMapImage( p.createTransformedImage().createImage(p.getMin(), p.getMax()), 1, 1);
2696 } else {
2697 return mipMap;
2700 } catch (Throwable e) {
2701 IJError.print(e);
2705 // else:
2706 Utils.log( "Loader.fetchDataImage: could not get a data image for patch " + p );
2707 return new MipMapImage( NOT_FOUND, p.getWidth() / NOT_FOUND.getWidth(), p.getHeight() / NOT_FOUND.getHeight() );
2711 public ImagePlus fetchImagePlus( Stack stack )
2713 ImagePlus imp = null;
2714 String path = null;
2715 ImageLoadingLock plock = null;
2716 synchronized (db_lock) {
2717 try {
2718 imp = mawts.get(stack.getId());
2719 if (null != imp) {
2720 return imp;
2722 path = stack.getFilePath();
2723 /* not cached */
2724 plock = getOrMakeImageLoadingLock( stack.getId(), 0 );
2725 } catch (Throwable t) {
2726 handleCacheError(t);
2727 return null;
2732 synchronized (plock) {
2733 imp = mawts.get( stack.getId());
2734 if (null != imp) {
2735 // was loaded by a different thread
2736 synchronized (db_lock) {
2737 removeImageLoadingLock(plock);
2739 return imp;
2742 // going to load:
2743 releaseToFit(stack.estimateImageFileSize());
2744 imp = openImage(getAbsolutePath(path));
2746 //preProcess(p, imp);
2749 synchronized (db_lock) {
2750 try {
2751 if (null == imp) {
2752 if (!hs_unloadable.contains(stack)) {
2753 Utils.log("FSLoader.fetchImagePlus: no image exists for stack " + stack + " at path " + path);
2754 hs_unloadable.add( stack );
2756 // if (ControlWindow.isGUIEnabled()) {
2757 // /* TODO offer repair for more things than patches */
2758 // FilePathRepair.add( stack );
2759 // }
2760 return null;
2761 } else {
2762 mawts.put( stack.getId(), imp, (int)Math.max(stack.getWidth(), stack.getHeight()));
2765 } catch (Exception e) {
2766 IJError.print(e);
2767 } finally {
2768 removeImageLoadingLock(plock);
2771 return imp;
2777 * Delete stale files under the {@link FSLoader#unuid} folder.
2778 * These include "*.ct" files (for {@link CoordinateTransform})
2779 * and "*.zip" files (for alpha mask images) that are not referenced from any {@link Patch}.
2781 @Override
2782 public boolean deleteStaleFiles(boolean coordinate_transforms, boolean alpha_masks) {
2783 boolean b = true;
2784 final Project project = Project.findProject(this);
2785 if (coordinate_transforms) b = b && StaleFiles.deleteCoordinateTransforms(project);
2786 if (alpha_masks) b = b && StaleFiles.deleteAlphaMasks(project);
2787 return b;
2791 ////////////////////
2794 static final public String[] MIPMAP_FORMATS = new String[]{".jpg", ".png", ".tif", ".raw", ".rag"};
2795 static public final int MIPMAP_JPEG = 0;
2796 static public final int MIPMAP_PNG = 1;
2797 static public final int MIPMAP_TIFF = 2;
2798 static public final int MIPMAP_RAW = 3;
2799 static public final int MIPMAP_RAG = 4;
2801 static private final int MIPMAP_HIGHEST = MIPMAP_RAG; // WARNING: update this value if other formats are added
2803 // Default: RAG
2804 private int mipmaps_format = MIPMAP_RAG;
2805 private String mExt = MIPMAP_FORMATS[mipmaps_format]; // the extension currently in use
2806 private RWImage mmio = new RWImageRag();
2808 private RWImage newMipMapRWImage() {
2809 switch (this.mipmaps_format) {
2810 case MIPMAP_JPEG:
2811 return new RWImageJPG();
2812 case MIPMAP_PNG:
2813 return new RWImagePNG();
2814 case MIPMAP_TIFF:
2815 return new RWImageTIFF();
2816 case MIPMAP_RAW:
2817 return new RWImageRaw();
2818 case MIPMAP_RAG:
2819 return new RWImageRag();
2820 // WARNING add here another one
2822 return null;
2825 /** Any of: {@link #MIPMAP_JPEG}, {@link #MIPMAP_PNG}, {@link #MIPMAP_TIFF}, {@link #MIPMAP_RAW},
2826 * {@link #MIPMAP_RAG}. */
2827 @Override
2828 public final int getMipMapFormat() {
2829 return mipmaps_format;
2832 @Override
2833 public final boolean setMipMapFormat(final int format) {
2834 switch (format) {
2835 case MIPMAP_JPEG:
2836 case MIPMAP_PNG:
2837 case MIPMAP_TIFF:
2838 case MIPMAP_RAW:
2839 case MIPMAP_RAG:
2840 this.mipmaps_format = format;
2841 this.mExt = MIPMAP_FORMATS[mipmaps_format];
2842 this.mmio = newMipMapRWImage();
2843 return true;
2844 default:
2845 Utils.log("Ignoring unknown mipmap format: " + format);
2846 return false;
2850 /** Removes all mipmap files and recreates them with the currently set mipmaps format.
2851 * @param old_format Any of MIPMAP_JPEG, MIPMAP_PNG in which files were saved before. */
2852 @Override
2853 public Bureaucrat updateMipMapsFormat(final int old_format, final int new_format) {
2854 if (old_format < 0 || old_format > MIPMAP_HIGHEST) {
2855 Utils.log("Invalid old format for mipmaps!");
2856 return null;
2858 if (!setMipMapFormat(new_format)) {
2859 Utils.log("Invalid new format for mipmaps!");
2860 return null;
2862 final Project project = Project.findProject(FSLoader.this);
2863 return Bureaucrat.createAndStart(new Worker.Task("Updating mipmaps format") {
2864 public void exec() {
2865 try {
2866 final List<Future<?>> fus = new ArrayList<Future<?>>();
2867 final String ext = MIPMAP_FORMATS[old_format];
2868 for (Layer la : project.getRootLayerSet().getLayers()) {
2869 for (Displayable p : la.getDisplayables(Patch.class)) {
2870 fus.add(removeMipMaps((Patch)p, ext));
2873 Utils.wait(fus);
2874 fus.clear();
2875 for (Layer la : project.getRootLayerSet().getLayers()) {
2876 for (Displayable p : la.getDisplayables(Patch.class)) {
2877 fus.add(regenerateMipMaps((Patch)p));
2880 Utils.wait(fus);
2881 } catch (Exception e) {
2882 IJError.print(e);
2885 }, project);
2888 private abstract class RWImage {
2889 boolean save(ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
2890 if (as_grey) ip = ip.convertToByte(false);
2891 if (ip instanceof ByteProcessor) {
2892 return save(path, new byte[][]{(byte[])ip.getPixels()}, ip.getWidth(), ip.getHeight(), quality);
2893 } else if (ip instanceof ColorProcessor) {
2894 final int[] p = (int[]) ip.getPixels();
2895 final byte[] r = new byte[p.length],
2896 g = new byte[p.length],
2897 b = new byte[p.length],
2898 a = new byte[p.length];
2899 for (int i=0; i<p.length; ++i) {
2900 final int x = p[i];
2901 r[i] = (byte)((x >> 16)&0xff);
2902 g[i] = (byte)((x >> 8)&0xff);
2903 b[i] = (byte) (x &0xff);
2904 a[i] = (byte)((x >> 24)&0xff);
2906 return save(path, new byte[][]{r, g, b, a}, ip.getWidth(), ip.getHeight(), quality);
2908 return false;
2910 boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
2911 switch (bi.getType()) {
2912 case BufferedImage.TYPE_BYTE_GRAY:
2913 return save(new ByteProcessor(bi), path, quality, false);
2914 default:
2915 if (as_grey) return save(new ByteProcessor(bi), path, quality, false);
2916 return save(new ColorProcessor(bi), path, quality, false);
2919 abstract boolean save(String path, byte[][] b, int width, int height, float quality);
2920 /** Opens grey, RGB and RGBA. */
2921 abstract BufferedImage open(String path);
2922 /** Opens grey images or, if not grey, converts them to grey. */
2923 abstract BufferedImage openGrey(String path);
2925 private final class RWImageJPG extends RWImage {
2926 @Override
2927 final boolean save(final ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
2928 return ImageSaver.saveAsJpeg(ip, path, quality, as_grey);
2930 @Override
2931 final boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
2932 return ImageSaver.saveAsJpeg(bi, path, quality, as_grey);
2934 @Override
2935 final BufferedImage open(String path) {
2936 return ImageSaver.openImage(path, true);
2938 @Override
2939 final BufferedImage openGrey(final String path) {
2940 return ImageSaver.open(path, true);
2942 @Override
2943 final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
2944 switch (b.length) {
2945 case 1:
2946 return ImageSaver.saveAsGreyJpeg(b[0], width, height, path, quality);
2947 case 2:
2948 return ImageSaver.saveAsJpegAlpha(ImageSaver.createARGBImage(P.blend(b[0], b[1]), width, height), path, quality);
2949 case 3:
2950 return ImageSaver.saveAsJpeg(ImageSaver.createRGBImage(P.blend(b[0], b[1], b[2]), width, height), path, quality, false);
2951 case 4:
2952 return ImageSaver.saveAsJpegAlpha(ImageSaver.createARGBImage(P.blend(b[0], b[1], b[2], b[3]), width, height), path, quality);
2954 return false;
2957 private final class RWImagePNG extends RWImage {
2958 @Override
2959 final boolean save(final ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
2960 return ImageSaver.saveAsPNG(ip, path);
2962 @Override
2963 final boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
2964 return ImageSaver.saveAsPNG(bi, path);
2966 @Override
2967 final BufferedImage open(String path) {
2968 return ImageSaver.openImage(path, true);
2970 @Override
2971 final BufferedImage openGrey(final String path) {
2972 return ImageSaver.openGreyImage(path);
2974 @Override
2975 final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
2976 BufferedImage bi = null;
2977 try {
2978 switch (b.length) {
2979 case 1:
2980 bi = ImageSaver.createGrayImage(b[0], width, height);
2981 return ImageSaver.saveAsPNG(bi, path);
2982 case 2:
2983 bi = ImageSaver.createARGBImage(P.blend(b[0], b[1]), width, height);
2984 return ImageSaver.saveAsPNG(bi, path);
2985 case 3:
2986 bi = ImageSaver.createRGBImage(P.blend(b[0], b[1], b[2]), width, height);
2987 return ImageSaver.saveAsPNG(bi, path);
2988 case 4:
2989 bi = ImageSaver.createARGBImage(P.blend(b[0], b[1], b[2], b[3]), width, height);
2990 return ImageSaver.saveAsPNG(bi, path);
2992 } finally {
2993 if (null != bi) {
2994 bi.flush();
2995 CachingThread.storeArrayForReuse(bi);
2998 return false;
3001 private final class RWImageTIFF extends RWImage {
3002 @Override
3003 final boolean save(final ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
3004 return ImageSaver.saveAsTIFF(ip, path, as_grey);
3006 @Override
3007 final boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
3008 return ImageSaver.saveAsTIFF(bi, path, as_grey);
3010 @Override
3011 final BufferedImage openGrey(final String path) {
3012 return ImageSaver.openGreyTIFF(path);
3014 @Override
3015 final BufferedImage open(String path) {
3016 return ImageSaver.openTIFF(path, true);
3018 @Override
3019 final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
3020 switch (b.length) {
3021 case 1:
3022 return ImageSaver.saveAsTIFF(ImageSaver.createGrayImage(b[0], width, height), path, false); // already grey
3023 case 2:
3024 return ImageSaver.saveAsTIFF(ImageSaver.createARGBImage(P.blend(b[0], b[1]), width, height), path, false);
3025 case 3:
3026 return ImageSaver.saveAsTIFF(ImageSaver.createRGBImage(P.blend(b[0], b[1], b[2]), width, height), path, false);
3027 case 4:
3028 return ImageSaver.saveAsTIFF(ImageSaver.createARGBImage(P.blend(b[0], b[1], b[2], b[3]), width, height), path, false);
3030 return false;
3033 private final class RWImageRaw extends RWImage {
3034 @Override
3035 final BufferedImage open(final String path) {
3036 return RawMipMaps.read(path);
3038 @Override
3039 final BufferedImage openGrey(final String path) {
3040 return ImageSaver.asGrey(RawMipMaps.read(path)); // TODO may not need the asGrey if all is correct
3042 @Override
3043 final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
3044 try {
3045 return RawMipMaps.save(path, b, width, height);
3046 } finally {
3047 CachingThread.storeForReuse(b);
3051 private final class RWImageRag extends RWImage {
3052 @Override
3053 final BufferedImage open(final String path) {
3054 return RagMipMaps.read(path);
3056 @Override
3057 final BufferedImage openGrey(final String path) {
3058 return ImageSaver.asGrey(RagMipMaps.read(path)); // TODO may not need the asGrey if all is correct
3060 @Override
3061 final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
3062 try {
3063 return RagMipMaps.save(path, b, width, height);
3064 } finally {
3065 CachingThread.storeForReuse(b);
3070 @SuppressWarnings("unchecked")
3071 @Override
3072 protected boolean mapIntensities(final Patch p, final ImagePlus imp) {
3074 final ImagePlus coefficients = new Opener().openImage(
3075 getUNUIdFolder() +
3076 "trakem2.its/" +
3077 createIdPath(Long.toString(p.getId()), "it", ".tif"));
3079 if (coefficients == null)
3080 return false;
3082 final ImageProcessor ip = imp.getProcessor();
3084 @SuppressWarnings({"rawtypes"})
3085 final LinearIntensityMap<FloatType> map =
3086 new LinearIntensityMap<FloatType>(
3087 (FloatImagePlus)ImagePlusImgs.from(coefficients));
3089 @SuppressWarnings("rawtypes")
3090 Img img;
3092 final long[] dims = new long[]{imp.getWidth(), imp.getHeight()};
3093 switch (p.getType()) {
3094 case ImagePlus.GRAY8:
3095 case ImagePlus.COLOR_256: // this only works for continuous color tables
3096 img = ArrayImgs.unsignedBytes((byte[])ip.getPixels(), dims);
3097 break;
3098 case ImagePlus.GRAY16:
3099 img = ArrayImgs.unsignedShorts((short[])ip.getPixels(), dims);
3100 break;
3101 case ImagePlus.COLOR_RGB:
3102 img = ArrayImgs.argbs((int[])ip.getPixels(), dims);
3103 break;
3104 case ImagePlus.GRAY32:
3105 img = ArrayImgs.floats((float[])ip.getPixels(), dims);
3106 break;
3107 default:
3108 img = null;
3111 if (img == null)
3112 return false;
3114 map.run(img);
3116 return true;
3119 @Override
3120 public boolean clearIntensityMap(final Patch p) {
3121 final File coefficients = new File(
3122 getUNUIdFolder() +
3123 "trakem2.its/" +
3124 createIdPath(Long.toString(p.getId()), "it", ".tif"));
3125 return coefficients.delete();