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.
23 package ini
.trakem2
.persistence
;
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
;
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
;
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
;
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. */
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
{
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
);
158 this.unuid
= createUNUId(this.dir_storage
);
159 createMipMapsDir(this.dir_storage
);
164 private String
createUNUId(String dir_storage
) {
165 synchronized (db_lock
) {
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()))
172 } catch (Exception e
) {
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.");
185 File f
= new File(dir_mipmaps
+ ".open.t2");
186 Utils
.log2("Crash detector file is " + dir_mipmaps
+ ".open.t2");
190 notifyMipMapsOutOfSynch();
192 if (!f
.createNewFile() && !dir_mipmaps
.startsWith("http:")) {
193 Utils
.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
195 Utils
.log2("Created crash detection system.");
198 } catch (Exception e
) {
199 Utils
.log2("Crash detector error:" + 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
;
227 dir_image_storage
= s
;
228 } catch (Exception e
) {
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)
241 path
= path
.replace('\\','/');
243 int itwo
= path
.indexOf("//");
245 if (0 == itwo
/* samba disk */
246 || (5 == itwo
&& "http:".equals(path
.substring(0, 5)))) {
249 path
= path
.substring(0, itwo
) + path
.substring(itwo
+1);
251 itwo
= path
.indexOf("//", itwo
+1);
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
);
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.");
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()) {
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();
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
);
302 } catch (Exception e
) {
306 if (null != i_stream
) {
309 } catch (Exception e
) {
315 if (null == handler
) {
316 Utils
.showMessage("Error when reading the project .xml file.");
320 data
= handler
.getProjectData(open_displays
);
324 Utils
.showMessage("Error when parsing the project .xml file.");
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
]);
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();
355 // 2 cores = 2 threads
356 // 3+ cores = cores-1 threads
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();
395 public synchronized void 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;
422 try { f
.delete(); } catch (Exception e
) { Utils
.log("Could not remove empty trakem2.mipmaps directory."); }
425 // remove crash detector
427 File fm
= new File(dir_mipmaps
+ ".open.t2");
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.");
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);
447 if (f
.isDirectory()) {
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. */
461 public long getNextId() {
463 synchronized (db_lock
) {
469 /** Get the next unique id to be used for the {@link Patch}'s {@link CoordinateTransform} or alpha mask. */
471 public long getNextBlobId() {
473 synchronized (db_lock
) {
479 /** Loaded in full from XML file */
480 public double[][][] fetchBezierArrays(long id
) {
484 /** Loaded in full from XML file */
485 public ArrayList
<?
> fetchPipePoints(long id
) {
489 /** Loaded in full from XML file */
490 public ArrayList
<?
> fetchBallPoints(long id
) {
494 /** Loaded in full from XML file */
495 public Area
fetchArea(long area_list_id
, long layer_id
) {
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;
517 ImageLoadingLock plock
= null;
518 synchronized (db_lock
) {
520 imp
= mawts
.get(p
.getId());
521 path
= getAbsolutePath(p
);
523 if (null != path
) i_sl
= path
.lastIndexOf("-----#slice=");
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());
536 case Layer
.IMAGEPROCESSOR
:
537 ip
= imp
.getStack().getProcessor(ia
);
539 case Layer
.IMAGEPLUS
:
542 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
547 return null; // beyond bonds!
551 // for non-stack images
554 case Layer
.IMAGEPROCESSOR
:
555 return imp
.getProcessor();
556 case Layer
.IMAGEPLUS
:
559 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
564 slice
= path
.substring(i_sl
);
566 path
= path
.substring(0, i_sl
);
569 plock
= getOrMakeImageLoadingLock(path
);
570 } catch (Throwable t
) {
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.
583 mawts
.put(p
.getId(), imp
, (int)Math
.max(p
.getWidth(), p
.getHeight()));
587 // was loaded by a different thread, or is shareable
589 case Layer
.IMAGEPROCESSOR
:
591 return imp
.getStack().getProcessor(Integer
.parseInt(slice
.substring(12)));
593 return imp
.getProcessor();
595 case Layer
.IMAGEPLUS
:
597 imp
.setSlice(Integer
.parseInt(slice
.substring(12)));
601 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
609 n_bytes
= estimateImageFileSize(p
, 0);
610 releaseToFit(n_bytes
);
611 imp
= openImage(path
);
613 preProcess(p
, imp
, n_bytes
);
615 synchronized (db_lock
) {
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
);
629 // set proper active slice
630 final int ia
= Integer
.parseInt(slice
.substring(12));
632 if (Layer
.IMAGEPROCESSOR
== format
) ip
= imp
.getStack().getProcessor(ia
); // otherwise creates one new for nothing
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
) {
647 case Layer
.IMAGEPROCESSOR
:
648 return ip
; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
649 case Layer
.IMAGEPLUS
:
652 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
660 /** Returns the alpha mask image from a file, or null if none stored. */
662 public ByteProcessor
fetchImageMask(final Patch p
) {
663 return p
.getAlphaMask();
667 synchronized public final String
getMasksFolder() {
668 if (null == dir_masks
) createMasksFolder();
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;
678 } catch (Exception e
) {
683 private String dir_cts
= null;
686 synchronized public final String
getCoordinateTransformsFolder() {
687 if (null == dir_cts
) createCoordinateTransformsFolder();
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;
697 } catch (Exception e
) {
702 /** Loaded in full from XML file */
703 public Object
[] fetchLabel(DLabel label
) {
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));
714 return openImage(original_path
);
715 } catch (Throwable t
) {
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
) {
727 final long id
= ob
.getId();
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
);
744 public boolean updateInDatabase(final DBObject ob
, final String key
) {
745 // Should only be GUI-driven
748 if (ob
.getClass() == Patch
.class) {
750 if (key
.equals("tiff_working")) return null != setImageFile(p
, fetchImagePlus(p
));
755 public boolean updateInDatabase(final DBObject ob
, final Set
<String
> keys
) {
756 // Should only be GUI-driven
758 if (ob
.getClass() == Patch
.class) {
760 if (keys
.contains("tiff_working")) return null != setImageFile(p
, fetchImagePlus(p
));
765 public boolean removeFromDatabase(final DBObject ob
) {
766 synchronized (db_lock
) {
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) {
773 // STRATEGY change: images are not owned by the FSLoader.
775 if (!ob
.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p
);
776 ht_paths
.remove(p
.getId()); // after removeMipMaps !
778 cannot_regenerate
.remove(p
);
779 flushMipMaps(p
.getId()); // locks on its own
780 touched_mipmaps
.remove(p
);
782 } catch (Throwable t
) {
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;
796 String path
= getAbsolutePath(p
);
799 // path can be null if the image is pasted, or from a copy, or totally new
801 int i_sl
= path
.lastIndexOf("-----#slice=");
803 slice
= path
.substring(i_sl
);
804 path
= path
.substring(0, i_sl
);
807 // no path, inspect image FileInfo's path if the image has no 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
);
816 Utils
.log2("Reusing image file: path exists for fileinfo at " + fipath
);
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
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
849 final int itag
= path
.lastIndexOf(tag
);
851 path
= path
.substring(0, itag
) + "." + i
+ tag
+ "tif";
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
865 updatePaths(p
, path2
, null != slice
);
867 hs_unloadable
.remove(p
);
870 Utils
.log("WARNING could not save image at " + path
);
872 updatePaths(p
, starting_path
, null != slice
);
876 } catch (Exception e
) {
882 /** Associate patch with imp, and all slices as well if any. */
883 private void cacheAll(final Patch p
, final ImagePlus imp
) {
885 for (Patch pa
: p
.getStackPatches()) {
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
) {
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
);
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
));
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
) {
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=");
932 slice
= path
.substring(i_sl
);
933 path
= path
.substring(0, i_sl
);
935 path
= getAbsolutePath(path
);
937 Utils
.log("Path for patch " + patch
+ " does not exist: " + path
);
940 // Else assume that it exists.
941 // reappend slice info if existent
942 if (null != slice
) path
+= slice
;
944 patch
.cacheCurrentPath(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()) {
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
) {
982 Utils
.log("Null path for patch: " + patch
);
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
);
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. */
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;
1021 this.project_file_path
= xml_path
;
1022 ControlWindow
.updateTitle(project
);
1023 result
= this.project_file_path
;
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();
1036 /** The saveAs called from menus via saveTask. */
1038 public String
saveAs(Project project
, XMLOptions options
) {
1039 String path
= super.saveAs(project
, null, options
);
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
);
1051 /** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
1053 public String
saveAs(final String path
, final XMLOptions options
) {
1055 Utils
.log("Cannot save on null path.");
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";
1063 // neither matches, add the default ".xml"
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
) {
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
);
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();
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
) {
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
) {
1132 protected void restorePaths(final Map
<Long
,String
> copy
, final String mipmaps_folder
, final String storage_folder
) {
1133 synchronized (db_lock
) {
1135 this.dir_mipmaps
= mipmaps_folder
;
1136 this.dir_storage
= storage_folder
;
1138 ht_paths
.putAll(copy
);
1139 } catch (Throwable 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
) {
1158 // fix W1nd0ws paths
1159 path
= path
.replace('\\', '/'); // char-based, no parsing problems
1161 String slice
= null;
1162 int isl
= path
.lastIndexOf("-----#slice");
1164 slice
= path
.substring(isl
);
1165 path
= path
.substring(0, isl
);
1168 if (FSLoader
.isRelativePath(path
)) {
1170 if (-1 != isl
) path
+= slice
;
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);
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());
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
);
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;
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('/')) {
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.");
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
);
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
= "";
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
);
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
);
1287 previous_patch
= patch
;
1288 Utils
.showProgress(i
* (1.0 / n
));
1290 Utils
.showProgress(1.0);
1292 // update calibration
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");
1311 String sf
= ob
.replace('\\', '/');
1312 if (isRelativePath(sf
)) {
1313 sf
= getParentFolder() + sf
;
1317 Utils
.log2("Can't have an URL as the path of a storage folder.");
1319 File f
= new File(sf
);
1320 if (f
.exists() && f
.isDirectory()) {
1321 this.dir_storage
= sf
;
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");
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");
1350 String mf
= ob
.replace('\\', '/');
1351 if (isRelativePath(mf
)) {
1352 mf
= getParentFolder() + mf
;
1355 this.dir_mipmaps
= mf
;
1356 // TODO must disable input somehow, so that images are not edited.
1358 File f
= new File(mf
);
1359 if (f
.exists() && f
.isDirectory()) {
1360 this.dir_mipmaps
= mf
;
1362 Utils
.log2("mipmaps_folder was not found or is invalid: " + ob
);
1366 ob
= ht_attributes
.remove("mipmaps_regen");
1368 this.mipmaps_regen
= Boolean
.parseBoolean(ob
);
1370 ob
= ht_attributes
.get("n_mipmap_threads");
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();
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. */
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() {
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];
1464 thresh_cm = new IndexColorModel(8, 256, c, c, c);
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);
1475 return pg
.getPixels();
1476 } catch (InterruptedException e
) {
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
);
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
);
1546 Utils
.log("generateMipMaps: null path for Patch " + patch
);
1547 cannot_regenerate
.add(patch
);
1550 if (hs_unloadable
.contains(patch
)) {
1551 FilePathRepair
.add(patch
);
1554 synchronized (gm_lock
) {
1556 if (null == dir_mipmaps
) createMipMapsDir(null);
1557 if (null == dir_mipmaps
|| isURL(dir_mipmaps
)) return false;
1558 } catch (Exception 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();
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
);
1594 alpha_mask
= pai
.mask
; // can be null
1595 outside_mask
= pai
.outside
; // can be null
1599 //final String filename = new StringBuilder(new File(path).getName()).append('.').append(patch.getId()).append(mExt).toString();
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
) {
1617 case ImagePlus
.COLOR_RGB
:
1618 case ImagePlus
.COLOR_256
:
1619 case ImagePlus
.GRAY8
:
1620 patch
.setMinAndMax(0, 255);
1622 // Find and flow through to default:
1623 case ImagePlus
.GRAY16
:
1624 ((ij
.process
.ShortProcessor
)ip
).findMinAndMax();
1625 patch
.setMinAndMax(ip
.getMin(), ip
.getMax());
1627 case ImagePlus
.GRAY32
:
1628 ((FloatProcessor
)ip
).findMinAndMax();
1629 patch
.setMinAndMax(ip
.getMin(), ip
.getMax());
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
);
1683 if (null != outside_mask
) {
1684 outside
= new FloatProcessorT2(outside_mask
);
1685 if ( null == alpha
) {
1687 alpha_mask
= outside_mask
;
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
);
1704 int k
= 0; // the scale level. Proper scale is: 1 / pow(2, k)
1706 if (Thread
.currentThread().isInterrupted()) return false;
1707 // 1 - Prepare values for the next scaled image
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
) {
1719 if (alpha
!= outside
)
1720 o
= gaussianBlurResizeInHalf(outside
); // idem
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;
1731 h
= red
.getHeight();
1733 // 4 - Compose ColorProcessor
1734 if (null == alpha
) {
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
);
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
);
1748 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
1751 long t0
= System
.currentTimeMillis();
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
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
);
1772 if (null != outside_mask
) {
1773 outside
= new FloatProcessorT2(outside_mask
);
1774 if (null == alpha
) {
1776 alpha_mask
= outside_mask
;
1782 int k
= 0; // the scale level. Proper scale is: 1 / pow(2, k)
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
);
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
);
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
);
1821 // 4 - prepare values for the next scaled image
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
));
1829 Utils
.log("ERROR: unknown image resizing mode for mipmaps: " + resizing_mode
);
1833 } catch (Throwable e
) {
1834 Utils
.log("*** ERROR: Can't generate mipmaps for patch " + patch
);
1836 cannot_regenerate
.add(patch
);
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());
1869 } catch (Exception e
) {
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
);
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),
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");
1918 Utils
.log2("Not a pointmatches.ser file: can't process " + path
);
1923 int ifolder
= path
.indexOf("pointmatches.ser/");
1925 Utils
.log2("Not in pointmatches.ser/ folder:" + path
);
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
);
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
);
1946 Utils
.log2("Could not delete " + f
.getAbsolutePath());
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
1959 islash
= dirname
.lastIndexOf('/');
1963 Utils
.log2("File does not exist: " + dir
+ name
);
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.)
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.");
1982 return Bureaucrat
.createAndStart(new Worker
.Task("Generating MipMaps") {
1983 public void exec() {
1984 this.setAsBackground(true);
1985 Utils
.log2("starting mipmap generation ..");
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
;
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();
1997 final String filename
= new File(getAbsolutePath(pa
)).getName() + "." + pa
.getId() + mExt
;
2002 if (!new File(dir_mipmaps
+ level
+ "/" + filename
).exists()) {
2006 } while (w
>= 32 && h
>= 32);
2009 fus
.add(regenerateMipMaps(pa
));
2014 } catch (Exception 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
)) {
2030 final File file
= new File(path
);
2031 if (file
.exists() && file
.isDirectory()) {
2038 } catch (Exception e
) {
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();
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
++) {
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);
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/";
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");
2106 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
2107 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
2110 } catch (Exception e
) {}
2111 } else if (f
.isDirectory()) {
2112 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
2113 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
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()) {
2130 this.dir_mipmaps
= parent_path
+ "trakem2." + unuid
+ "/trakem2.mipmaps/";
2132 File f
= new File(this.dir_mipmaps
);
2135 Utils
.log("Could not create trakem2.mipmaps!");
2138 } catch (Exception e
) {
2143 Utils
.showMessage("Selected parent path is not a directory. Please choose another one.");
2144 return createMipMapsDir(null);
2147 Utils
.showMessage("Parent path does not exist. Please select a new one.");
2148 return createMipMapsDir(null);
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
) {
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
) {
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() {
2191 final String path
= getAbsolutePath(p
);
2194 Utils
.log2("Remover: null path for Patch " + p
);
2197 removeMipMaps(createIdPath(Long
.toString(p
.getId()), new File(path
).getName(), extension
), width
, height
);
2198 flushMipMaps(p
.getId());
2200 } catch (Exception e
) {
2208 private void removeMipMaps(final String filename
, final int width
, final int height
) {
2211 int k
= 0; // the level
2213 final File f
= new File(new StringBuilder(dir_mipmaps
).append(k
).append('/').append(filename
).toString());
2217 Utils
.log2("Could not remove file " + f
.getAbsolutePath());
2219 } catch (Exception e
) {
2226 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
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.
2238 public int getClosestMipMapLevel(final Patch patch
, int level
, final int max_level
) {
2239 if (null == dir_mipmaps
) return 0;
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
;
2252 final File f
= new File(new StringBuilder(dir_mipmaps
).append(level
).append('/').append(filename
).toString());
2256 // try the next level
2258 } while (level
>= 0);
2260 } catch (Exception e
) {
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;
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!");
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
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
);
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");
2342 while (retries
< MAX_RETRIES
) {
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
) {
2354 // check that REALLY the file doesn't exist.
2355 if (cannot_regenerate
.contains(patch
)) {
2356 Utils
.log("Cannot regenerate mipmaps for patch " + patch
);
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");
2374 return fetchMipMapAWT(patch
, level
, n_bytes
, retries
+ 1);
2375 } catch (Throwable t
) {
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
>
2392 public boolean cancel(boolean mayInterruptIfRunning
) {
2396 public Boolean
get() throws InterruptedException
, ExecutionException
{
2400 public Boolean
get(long timeout
, TimeUnit unit
)
2401 throws InterruptedException
, ExecutionException
,
2406 public boolean isCancelled() {
2410 public boolean isDone() {
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. */
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());
2426 synchronized (gm_lock
) {
2428 Future
<Boolean
> fu
= regenerating_mipmaps
.get(patch
);
2429 if (null != fu
) return fu
;
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() {
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
) {
2455 n_regenerating
.decrementAndGet();
2460 regenerating_mipmaps
.put(patch
, fu
);
2464 } catch (Exception e
) {
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
) {
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();
2482 case ImagePlus
.GRAY32
:
2483 bytes_per_pixel
= 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
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;
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[]
2498 bytes_per_pixel
= 5; // conservative
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
);
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() {
2536 // 1 - Create folder unuid_folder at storage_folder + unuid
2537 if (null == this.unuid
) {
2538 Utils
.log2("No unuid for project!");
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
);
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
);
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();
2576 Utils
.log2("Could not create parent dir " + fd
.getAbsolutePath());
2579 if (!oldf
.renameTo(newf
)) {
2580 Utils
.log2("Could not move mipmap file " + oldf
.getAbsolutePath() + " to " + newf
.getAbsolutePath());
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();
2611 Utils
.log2("Could not create parent dir " + fd
.getAbsolutePath());
2614 if (!fmask
.renameTo(newf
)) {
2615 Utils
.log2("Could not move mask file " + fmask
.getAbsolutePath() + " to " + newf
.getAbsolutePath());
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!
2630 } catch (Exception e
) {
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() {
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.
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
);
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
) {
2678 synchronized (gm_lock
) {
2679 fu
= regenerating_mipmaps
.get(p
);
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);
2700 } catch (Throwable e
) {
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;
2715 ImageLoadingLock plock
= null;
2716 synchronized (db_lock
) {
2718 imp
= mawts
.get(stack
.getId());
2722 path
= stack
.getFilePath();
2724 plock
= getOrMakeImageLoadingLock( stack
.getId(), 0 );
2725 } catch (Throwable t
) {
2726 handleCacheError(t
);
2732 synchronized (plock
) {
2733 imp
= mawts
.get( stack
.getId());
2735 // was loaded by a different thread
2736 synchronized (db_lock
) {
2737 removeImageLoadingLock(plock
);
2743 releaseToFit(stack
.estimateImageFileSize());
2744 imp
= openImage(getAbsolutePath(path
));
2746 //preProcess(p, imp);
2749 synchronized (db_lock
) {
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 );
2762 mawts
.put( stack
.getId(), imp
, (int)Math
.max(stack
.getWidth(), stack
.getHeight()));
2765 } catch (Exception e
) {
2768 removeImageLoadingLock(plock
);
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}.
2782 public boolean deleteStaleFiles(boolean coordinate_transforms
, boolean alpha_masks
) {
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
);
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
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
) {
2811 return new RWImageJPG();
2813 return new RWImagePNG();
2815 return new RWImageTIFF();
2817 return new RWImageRaw();
2819 return new RWImageRag();
2820 // WARNING add here another one
2825 /** Any of: {@link #MIPMAP_JPEG}, {@link #MIPMAP_PNG}, {@link #MIPMAP_TIFF}, {@link #MIPMAP_RAW},
2826 * {@link #MIPMAP_RAG}. */
2828 public final int getMipMapFormat() {
2829 return mipmaps_format
;
2833 public final boolean setMipMapFormat(final int format
) {
2840 this.mipmaps_format
= format
;
2841 this.mExt
= MIPMAP_FORMATS
[mipmaps_format
];
2842 this.mmio
= newMipMapRWImage();
2845 Utils
.log("Ignoring unknown mipmap format: " + format
);
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. */
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!");
2858 if (!setMipMapFormat(new_format
)) {
2859 Utils
.log("Invalid new format for mipmaps!");
2862 final Project project
= Project
.findProject(FSLoader
.this);
2863 return Bureaucrat
.createAndStart(new Worker
.Task("Updating mipmaps format") {
2864 public void exec() {
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
));
2875 for (Layer la
: project
.getRootLayerSet().getLayers()) {
2876 for (Displayable p
: la
.getDisplayables(Patch
.class)) {
2877 fus
.add(regenerateMipMaps((Patch
)p
));
2881 } catch (Exception e
) {
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
) {
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
);
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);
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
{
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
);
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
);
2935 final BufferedImage
open(String path
) {
2936 return ImageSaver
.openImage(path
, true);
2939 final BufferedImage
openGrey(final String path
) {
2940 return ImageSaver
.open(path
, true);
2943 final boolean save(final String path
, final byte[][] b
, final int width
, final int height
, final float quality
) {
2946 return ImageSaver
.saveAsGreyJpeg(b
[0], width
, height
, path
, quality
);
2948 return ImageSaver
.saveAsJpegAlpha(ImageSaver
.createARGBImage(P
.blend(b
[0], b
[1]), width
, height
), path
, quality
);
2950 return ImageSaver
.saveAsJpeg(ImageSaver
.createRGBImage(P
.blend(b
[0], b
[1], b
[2]), width
, height
), path
, quality
, false);
2952 return ImageSaver
.saveAsJpegAlpha(ImageSaver
.createARGBImage(P
.blend(b
[0], b
[1], b
[2], b
[3]), width
, height
), path
, quality
);
2957 private final class RWImagePNG
extends RWImage
{
2959 final boolean save(final ImageProcessor ip
, final String path
, final float quality
, final boolean as_grey
) {
2960 return ImageSaver
.saveAsPNG(ip
, path
);
2963 final boolean save(final BufferedImage bi
, final String path
, final float quality
, final boolean as_grey
) {
2964 return ImageSaver
.saveAsPNG(bi
, path
);
2967 final BufferedImage
open(String path
) {
2968 return ImageSaver
.openImage(path
, true);
2971 final BufferedImage
openGrey(final String path
) {
2972 return ImageSaver
.openGreyImage(path
);
2975 final boolean save(final String path
, final byte[][] b
, final int width
, final int height
, final float quality
) {
2976 BufferedImage bi
= null;
2980 bi
= ImageSaver
.createGrayImage(b
[0], width
, height
);
2981 return ImageSaver
.saveAsPNG(bi
, path
);
2983 bi
= ImageSaver
.createARGBImage(P
.blend(b
[0], b
[1]), width
, height
);
2984 return ImageSaver
.saveAsPNG(bi
, path
);
2986 bi
= ImageSaver
.createRGBImage(P
.blend(b
[0], b
[1], b
[2]), width
, height
);
2987 return ImageSaver
.saveAsPNG(bi
, path
);
2989 bi
= ImageSaver
.createARGBImage(P
.blend(b
[0], b
[1], b
[2], b
[3]), width
, height
);
2990 return ImageSaver
.saveAsPNG(bi
, path
);
2995 CachingThread
.storeArrayForReuse(bi
);
3001 private final class RWImageTIFF
extends RWImage
{
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
);
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
);
3011 final BufferedImage
openGrey(final String path
) {
3012 return ImageSaver
.openGreyTIFF(path
);
3015 final BufferedImage
open(String path
) {
3016 return ImageSaver
.openTIFF(path
, true);
3019 final boolean save(final String path
, final byte[][] b
, final int width
, final int height
, final float quality
) {
3022 return ImageSaver
.saveAsTIFF(ImageSaver
.createGrayImage(b
[0], width
, height
), path
, false); // already grey
3024 return ImageSaver
.saveAsTIFF(ImageSaver
.createARGBImage(P
.blend(b
[0], b
[1]), width
, height
), path
, false);
3026 return ImageSaver
.saveAsTIFF(ImageSaver
.createRGBImage(P
.blend(b
[0], b
[1], b
[2]), width
, height
), path
, false);
3028 return ImageSaver
.saveAsTIFF(ImageSaver
.createARGBImage(P
.blend(b
[0], b
[1], b
[2], b
[3]), width
, height
), path
, false);
3033 private final class RWImageRaw
extends RWImage
{
3035 final BufferedImage
open(final String path
) {
3036 return RawMipMaps
.read(path
);
3039 final BufferedImage
openGrey(final String path
) {
3040 return ImageSaver
.asGrey(RawMipMaps
.read(path
)); // TODO may not need the asGrey if all is correct
3043 final boolean save(final String path
, final byte[][] b
, final int width
, final int height
, final float quality
) {
3045 return RawMipMaps
.save(path
, b
, width
, height
);
3047 CachingThread
.storeForReuse(b
);
3051 private final class RWImageRag
extends RWImage
{
3053 final BufferedImage
open(final String path
) {
3054 return RagMipMaps
.read(path
);
3057 final BufferedImage
openGrey(final String path
) {
3058 return ImageSaver
.asGrey(RagMipMaps
.read(path
)); // TODO may not need the asGrey if all is correct
3061 final boolean save(final String path
, final byte[][] b
, final int width
, final int height
, final float quality
) {
3063 return RagMipMaps
.save(path
, b
, width
, height
);
3065 CachingThread
.storeForReuse(b
);
3070 @SuppressWarnings("unchecked")
3072 protected boolean mapIntensities(final Patch p
, final ImagePlus imp
) {
3074 final ImagePlus coefficients
= new Opener().openImage(
3077 createIdPath(Long
.toString(p
.getId()), "it", ".tif"));
3079 if (coefficients
== null)
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")
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
);
3098 case ImagePlus
.GRAY16
:
3099 img
= ArrayImgs
.unsignedShorts((short[])ip
.getPixels(), dims
);
3101 case ImagePlus
.COLOR_RGB
:
3102 img
= ArrayImgs
.argbs((int[])ip
.getPixels(), dims
);
3104 case ImagePlus
.GRAY32
:
3105 img
= ArrayImgs
.floats((float[])ip
.getPixels(), dims
);
3120 public boolean clearIntensityMap(final Patch p
) {
3121 final File coefficients
= new File(
3124 createIdPath(Long
.toString(p
.getId()), "it", ".tif"));
3125 return coefficients
.delete();