3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 /s published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
23 package ini
.trakem2
.persistence
;
27 import ij
.VirtualStack
; // only after 1.38q
29 import ij
.process
.ByteProcessor
;
30 import ij
.process
.ImageProcessor
;
31 import ij
.process
.FloatProcessor
;
32 import ij
.process
.ColorProcessor
;
33 import ini
.trakem2
.Project
;
34 import ini
.trakem2
.ControlWindow
;
35 import ini
.trakem2
.display
.DLabel
;
36 import ini
.trakem2
.display
.Display
;
37 import ini
.trakem2
.display
.Displayable
;
38 import ini
.trakem2
.display
.Layer
;
39 import ini
.trakem2
.display
.Patch
;
40 import ini
.trakem2
.display
.YesNoDialog
;
41 import ij
.gui
.YesNoCancelDialog
;
42 import ini
.trakem2
.utils
.*;
43 import ini
.trakem2
.io
.*;
44 import ini
.trakem2
.imaging
.FloatProcessorT2
;
46 import java
.awt
.Graphics2D
;
47 import java
.awt
.Image
;
48 import java
.awt
.image
.BufferedImage
;
49 import java
.awt
.image
.IndexColorModel
;
50 import java
.awt
.image
.ColorModel
;
51 import java
.awt
.image
.PixelGrabber
;
52 import java
.awt
.RenderingHints
;
53 import java
.awt
.geom
.Area
;
54 import java
.awt
.geom
.AffineTransform
;
55 import java
.io
.BufferedInputStream
;
57 import java
.io
.FileInputStream
;
58 import java
.io
.FilenameFilter
;
59 import java
.io
.InputStream
;
62 import javax
.swing
.JMenuItem
;
63 import javax
.swing
.JMenu
;
64 import java
.awt
.event
.ActionListener
;
65 import java
.awt
.event
.ActionEvent
;
66 import java
.awt
.event
.KeyEvent
;
67 import javax
.swing
.KeyStroke
;
69 import org
.xml
.sax
.InputSource
;
71 import javax
.xml
.parsers
.SAXParserFactory
;
72 import javax
.xml
.parsers
.SAXParser
;
74 import mpi
.fruitfly
.math
.datastructures
.FloatArray2D
;
75 import mpi
.fruitfly
.registration
.ImageFilter
;
76 import mpi
.fruitfly
.general
.MultiThreading
;
78 import java
.util
.concurrent
.atomic
.AtomicInteger
;
79 import java
.util
.concurrent
.ExecutorService
;
80 import java
.util
.concurrent
.Executors
;
81 import java
.util
.concurrent
.ThreadPoolExecutor
;
82 import java
.util
.regex
.Pattern
;
85 /** 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. */
86 public final class FSLoader
extends Loader
{
88 /** Largest id seen so far. */
89 private long max_id
= -1;
90 private final HashMap
<Long
,String
> ht_paths
= new HashMap
<Long
,String
>();
91 /** For saving and overwriting. */
92 private String project_file_path
= null;
93 /** Path to the directory hosting the file image pyramids. */
94 private String dir_mipmaps
= null;
95 /** Path to the directory the user provided when creating the project. */
96 private String dir_storage
= null;
97 /** Path to the directory hosting the alpha masks. */
98 private String dir_masks
= null;
100 /** Path to dir_storage + "trakem2.images/" */
101 private String dir_image_storage
= null;
103 /** Queue and execute Runnable tasks. */
104 static private Dispatcher dispatcher
= new Dispatcher();
106 private Set
<Patch
> touched_mipmaps
= Collections
.synchronizedSet(new HashSet
<Patch
>());
108 /** Used to open a project from an existing XML file. */
111 super.v_loaders
.remove(this); //will be readded on successful open
112 FSLoader
.startStaticServices();
115 private String unuid
= null;
117 /** Used to create a new project, NOT from an XML file. */
118 public FSLoader(final String storage_folder
) {
120 if (null == storage_folder
) this.dir_storage
= super.getStorageFolder(); // home dir
121 else this.dir_storage
= storage_folder
;
122 if (!this.dir_storage
.endsWith("/")) this.dir_storage
+= "/";
123 if (!Loader
.canReadAndWriteTo(dir_storage
)) {
124 Utils
.log("WARNING can't read/write to the storage_folder at " + dir_storage
);
126 this.unuid
= createUNUId(this.dir_storage
);
127 createMipMapsDir(this.dir_storage
);
132 private String
createUNUId(String dir_storage
) {
133 synchronized (db_lock
) {
136 if (null == dir_storage
) dir_storage
= System
.getProperty("user.dir") + "/";
137 return new StringBuffer(64).append(System
.currentTimeMillis()).append('.')
138 .append(Math
.abs(dir_storage
.hashCode())).append('.')
139 .append(Math
.abs(System
.getProperty("user.name").hashCode()))
141 } catch (Exception e
) {
150 /** Create a new FSLoader copying some key parameters such as preprocessor plugin, and storage and mipmap folders. Used for creating subprojects. */
151 public FSLoader(final Loader source
) {
153 this.dir_storage
= source
.getStorageFolder(); // can never be null
154 this.dir_mipmaps
= source
.getMipMapsFolder();
155 if (null == this.dir_mipmaps
) createMipMapsDir(this.dir_storage
);
156 setPreprocessor(source
.getPreprocessor());
159 /** 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. */
160 private void crashDetector() {
161 if (null == dir_mipmaps
) {
162 Utils
.log2("Could NOT create crash detection system: null dir_mipmaps.");
165 File f
= new File(dir_mipmaps
+ ".open.t2");
166 Utils
.log2("Crash detector file is " + dir_mipmaps
+ ".open.t2");
170 askAndExecMipmapRegeneration("TrakEM2 detected a crash!");
172 if (!f
.createNewFile() && !dir_mipmaps
.startsWith("http:")) {
173 Utils
.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
175 Utils
.log2("Created crash detection system.");
178 } catch (Exception e
) {
179 Utils
.log2("Crash detector error:" + e
);
184 public String
getProjectXMLPath() {
185 if (null == project_file_path
) return null;
186 return project_file_path
.toString(); // a copy of it
189 /** 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. */
190 public String
getStorageFolder() {
191 if (null == dir_storage
) return super.getStorageFolder(); // the user's home
192 return dir_storage
.toString(); // a copy
195 /** Returns a folder proven to be writable for images can be stored into. */
196 public String
getImageStorageFolder() {
197 if (null == dir_image_storage
) {
198 String s
= getUNUIdFolder() + "trakem2.images/";
199 File f
= new File(s
);
200 if (f
.exists() && f
.isDirectory() && f
.canWrite()) {
201 dir_image_storage
= s
;
202 return dir_image_storage
;
207 dir_image_storage
= s
;
208 } catch (Exception e
) {
210 return getStorageFolder(); // fall back
214 return dir_image_storage
;
217 /** Returns TMLHandler.getProjectData() . If the path is null it'll be asked for. */
218 public Object
[] openFSProject(String path
, final boolean open_displays
) {
219 // clean path of double-slashes, safely (and painfully)
221 path
= path
.replace('\\','/');
223 int itwo
= path
.indexOf("//");
225 if (0 == itwo
/* samba disk */
226 || (5 == itwo
&& "http:".equals(path
.substring(0, 5)))) {
229 path
= path
.substring(0, itwo
) + path
.substring(itwo
+1);
231 itwo
= path
.indexOf("//", itwo
+1);
236 String user
= System
.getProperty("user.name");
237 OpenDialog od
= new OpenDialog("Select Project", OpenDialog
.getDefaultDirectory(), null);
238 String file
= od
.getFileName();
239 if (null == file
|| file
.toLowerCase().startsWith("null")) return null;
240 String dir
= od
.getDirectory().replace('\\', '/');
241 if (!dir
.endsWith("/")) dir
+= "/";
242 this.project_file_path
= dir
+ file
;
243 Utils
.log2("project file path 1: " + this.project_file_path
);
245 this.project_file_path
= path
;
246 Utils
.log2("project file path 2: " + this.project_file_path
);
248 Utils
.log2("Loader.openFSProject: path is " + path
);
249 // check if any of the open projects uses the same file path, and refuse to open if so:
250 if (null != FSLoader
.getOpenProject(project_file_path
, this)) {
251 Utils
.showMessage("The project is already open.");
255 Object
[] data
= null;
257 // parse file, according to expected format as indicated by the extension:
258 if (this.project_file_path
.toLowerCase().endsWith(".xml")) {
259 InputStream i_stream
= null;
260 TMLHandler handler
= new TMLHandler(this.project_file_path
, this);
261 if (handler
.isUnreadable()) {
265 SAXParserFactory factory
= SAXParserFactory
.newInstance();
266 factory
.setValidating(true);
267 SAXParser parser
= factory
.newSAXParser();
268 if (isURL(this.project_file_path
)) {
269 i_stream
= new java
.net
.URL(this.project_file_path
).openStream();
271 i_stream
= new BufferedInputStream(new FileInputStream(this.project_file_path
));
273 InputSource input_source
= new InputSource(i_stream
);
274 setMassiveMode(true);
275 parser
.parse(input_source
, handler
);
276 } catch (java
.io
.FileNotFoundException fnfe
) {
277 Utils
.log("ERROR: File not found: " + path
);
279 } catch (Exception e
) {
283 setMassiveMode(false);
284 if (null != i_stream
) {
287 } catch (Exception e
) {
293 if (null == handler
) {
294 Utils
.showMessage("Error when reading the project .xml file.");
298 data
= handler
.getProjectData(open_displays
);
302 Utils
.showMessage("Error when parsing the project .xml file.");
306 super.v_loaders
.add(this);
311 // Only one thread at a time may access this method.
312 synchronized static private final Project
getOpenProject(final String project_file_path
, final Loader caller
) {
313 if (null == v_loaders
) return null;
314 final Loader
[] lo
= (Loader
[])v_loaders
.toArray(new Loader
[0]); // atomic way to get the list of loaders
315 for (int i
=0; i
<lo
.length
; i
++) {
316 if (lo
[i
].equals(caller
)) continue;
317 if (lo
[i
] instanceof FSLoader
&& ((FSLoader
)lo
[i
]).project_file_path
.equals(project_file_path
)) {
318 return Project
.findProject(lo
[i
]);
324 static public final Project
getOpenProject(final String project_file_path
) {
325 return getOpenProject(project_file_path
, null);
328 public boolean isReady() {
329 return null != ht_paths
;
332 static private void startStaticServices() {
333 if (null == dispatcher
|| dispatcher
.isQuit()) dispatcher
= new Dispatcher();
334 int np
= Runtime
.getRuntime().availableProcessors();
336 // 2 cores = 2 threads
337 // 3+ cores = cores-1 threads
339 if (null == regenerator
|| regenerator
.isShutdown()) {
340 regenerator
= Executors
.newFixedThreadPool(np
);
342 if (null == repainter
|| repainter
.isShutdown()) {
343 repainter
= Executors
.newFixedThreadPool(np
); // for SnapshotPanel
347 /** Shutdown the various thread pools and disactivate services in general. */
348 static private void destroyStaticServices() {
349 if (null != regenerator
) regenerator
.shutdownNow();
350 if (null != dispatcher
) dispatcher
.quit();
351 if (null != repainter
) repainter
.shutdownNow();
354 public void destroy() {
356 Utils
.showStatus("", false);
357 // delete mipmap files that where touched and not cleared as saved (i.e. the project was not saved)
358 for (final Patch p
: touched_mipmaps
) {
359 File f
= new File(getAbsolutePath(p
));
360 Utils
.log2("File f is " + f
);
361 if (f
.exists()) { // TODO this may not work for stacks!
362 Utils
.log2("Removing mipmaps for " + p
);
363 // Cannot run in the dispatcher: is a daemon, and would be interrupted.
364 removeMipMaps(createIdPath(Long
.toString(p
.getId()), f
.getName(), ".jpg"), (int)p
.getWidth(), (int)p
.getHeight()); // needs the dispatcher!
368 // remove empty trakem2.mipmaps folder if any
369 if (null != dir_mipmaps
&& !dir_mipmaps
.equals(dir_storage
)) {
370 File f
= new File(dir_mipmaps
);
371 if (f
.isDirectory() && 0 == f
.list(new FilenameFilter() {
372 public boolean accept(File fdir
, String name
) {
373 File file
= new File(dir_mipmaps
+ name
);
374 if (file
.isHidden() || '.' == name
.charAt(0)) return false;
378 try { f
.delete(); } catch (Exception e
) { Utils
.log("Could not remove empty trakem2.mipmaps directory."); }
381 // remove crash detector
382 File f
= new File(dir_mipmaps
+ ".open.t2");
385 Utils
.log2("WARNING: could not delete crash detector file .open.t2 from trakem2.mipmaps folder at " + dir_mipmaps
);
387 } catch (Exception e
) {
388 Utils
.log2("WARNING: crash detector file trakem.mipmaps/.open.t2 may NOT have been deleted.");
391 if (null == ControlWindow
.getProjects() || 1 == ControlWindow
.getProjects().size()) {
392 destroyStaticServices();
396 /** Get the next unique id, not shared by any other object within the same project. */
397 public long getNextId() {
399 synchronized (db_lock
) {
407 /** Loaded in full from XML file */
408 public double[][][] fetchBezierArrays(long id
) {
412 /** Loaded in full from XML file */
413 public ArrayList
fetchPipePoints(long id
) {
417 /** Loaded in full from XML file */
418 public ArrayList
fetchBallPoints(long id
) {
422 /** Loaded in full from XML file */
423 public Area
fetchArea(long area_list_id
, long layer_id
) {
427 /* Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImagePlust.getProcessor().
428 * or just use the Patch.getImageProcessor() method which does it for you. */
429 public ImagePlus
fetchImagePlus(final Patch p
) {
430 return (ImagePlus
)fetchImage(p
, Layer
.IMAGEPLUS
);
433 /** Fetch the ImageProcessor in a synchronized manner, so that there are no conflicts in retrieving the ImageProcessor for a specific stack slice, for example.
434 * Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImageProcessor,
435 * or just use the Patch.getImageProcessor() method which does it for you. */
436 public ImageProcessor
fetchImageProcessor(final Patch p
) {
437 return (ImageProcessor
)fetchImage(p
, Layer
.IMAGEPROCESSOR
);
440 /** So far accepts Layer.IMAGEPLUS and Layer.IMAGEPROCESSOR as format. */
441 public Object
fetchImage(final Patch p
, final int format
) {
442 ImagePlus imp
= null;
443 ImageProcessor ip
= null;
447 PatchLoadingLock plock
= null;
448 synchronized (db_lock
) {
450 imp
= imps
.get(p
.getId());
452 path
= getAbsolutePath(p
);
454 if (null != path
) i_sl
= path
.lastIndexOf("-----#slice=");
456 // activate proper slice
458 // check that the stack is large enough (user may have changed it)
459 final int ia
= Integer
.parseInt(path
.substring(i_sl
+ 12));
460 if (ia
<= imp
.getNSlices()) {
461 if (null == imp
.getStack() || null == imp
.getStack().getPixels(ia
)) {
462 // reload (happens when closing a stack that was opened before importing it, and then trying to paint, for example)
463 imps
.remove(p
.getId());
468 case Layer
.IMAGEPROCESSOR
:
469 ip
= imp
.getStack().getProcessor(ia
);
472 case Layer
.IMAGEPLUS
:
476 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
482 return null; // beyond bonds!
486 // for non-stack images
490 case Layer
.IMAGEPROCESSOR
:
491 return imp
.getProcessor();
492 case Layer
.IMAGEPLUS
:
495 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
500 slice
= path
.substring(i_sl
);
502 path
= path
.substring(0, i_sl
);
505 releaseMemory(); // ensure there is a minimum % of free memory
506 plock
= getOrMakePatchLoadingLock(p
, 0);
507 } catch (Exception e
) {
516 synchronized (plock
) {
519 imp
= imps
.get(p
.getId());
521 // was loaded by a different thread -- TODO the slice of the stack could be wrong!
524 case Layer
.IMAGEPROCESSOR
:
525 return imp
.getProcessor();
526 case Layer
.IMAGEPLUS
:
529 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
538 synchronized (db_lock
) {
540 n_bytes
= estimateImageFileSize(p
, 0);
541 max_memory
-= n_bytes
;
545 releaseToFit(n_bytes
);
546 imp
= openImage(path
);
550 synchronized (db_lock
) {
553 max_memory
+= n_bytes
;
556 if (!hs_unloadable
.contains(p
)) {
557 Utils
.log("FSLoader.fetchImagePlus: no image exists for patch " + p
+ " at path " + path
);
558 hs_unloadable
.add(p
);
560 if (ControlWindow
.isGUIEnabled()) {
561 FilePathRepair
.add(p
);
563 removePatchLoadingLock(plock
);
568 // update all clients of the stack, if any
570 String rel_path
= getPath(p
); // possibly relative
571 final int r_isl
= rel_path
.lastIndexOf("-----#slice");
572 if (-1 != r_isl
) rel_path
= rel_path
.substring(0, r_isl
); // should always happen
573 for (Iterator
<Map
.Entry
<Long
,String
>> it
= ht_paths
.entrySet().iterator(); it
.hasNext(); ) {
574 final Map
.Entry
<Long
,String
> entry
= it
.next();
575 final String str
= entry
.getValue(); // this is like calling getPath(p)
576 //Utils.log2("processing " + str);
577 if (0 != str
.indexOf(rel_path
)) {
578 //Utils.log2("SKIP str is: " + str + "\t but path is: " + rel_path);
579 continue; // get only those whose path is identical, of course!
581 final int isl
= str
.lastIndexOf("-----#slice=");
583 //int i_slice = Integer.parseInt(str.substring(isl + 12));
584 final long lid
= entry
.getKey();
588 // set proper active slice
589 final int ia
= Integer
.parseInt(slice
.substring(12));
591 if (Layer
.IMAGEPROCESSOR
== format
) ip
= imp
.getStack().getProcessor(ia
); // otherwise creates one new for nothing
593 // for non-stack images
594 // OBSOLETE and wrong //p.putMinAndMax(imp); // non-destructive contrast: min and max -- WRONG, it's destructive for ColorProcessor and ByteProcessor!
595 // puts the Patch min and max values into the ImagePlus processor.
596 imps
.put(p
.getId(), imp
);
597 if (Layer
.IMAGEPROCESSOR
== format
) ip
= imp
.getProcessor();
599 // imp is cached, so:
600 removePatchLoadingLock(plock
);
602 } catch (Exception e
) {
608 case Layer
.IMAGEPROCESSOR
:
609 return ip
; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
610 case Layer
.IMAGEPLUS
:
613 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
621 /** Returns the alpha mask image from a file, or null if none stored. */
623 public ByteProcessor
fetchImageMask(final Patch p
) {
624 // Else, see if there is a file for the Patch:
625 final String path
= getAlphaPath(p
);
626 if (null == path
) return null;
627 // Open the mask image, which should be a compressed float tif.
628 final ImagePlus imp
= opener
.openImage(path
);
630 //Utils.log2("No mask found or could not open mask image for patch " + p + " from " + path);
633 final ByteProcessor mask
= (ByteProcessor
)imp
.getProcessor().convertToByte(false);
634 //Utils.log2("Mask dimensions: " + mask.getWidth() + " x " + mask.getHeight() + " for patch " + p);
635 if (mask
.getWidth() != p
.getOWidth() || mask
.getHeight() != p
.getOHeight()) {
636 Utils
.log2("Mask has improper dimensions: " + mask
.getWidth() + " x " + mask
.getHeight() + " for patch " + p
+ " which is of " + p
.getOWidth() + " x " + p
.getOHeight());
643 public String
getAlphaPath(final Patch p
) {
644 final String filename
= getInternalFileName(p
);
645 if (null == filename
) {
646 Utils
.log2("null filepath!");
649 final String dir
= getMasksFolder();
650 return new StringBuffer(dir
).append(createIdPath(Long
.toString(p
.getId()), filename
, ".zip")).toString();
654 public void storeAlphaMask(final Patch p
, final ByteProcessor fp
) {
655 // would fail if user deletes the trakem2.masks/ folder from the storage folder after having set dir_masks. But that is his problem.
656 final String path
= getAlphaPath(p
);
657 File parent
= new File(path
).getParentFile();
659 IJ
.redirectErrorMessages();
660 new FileSaver(new ImagePlus("mask", fp
)).saveAsZip(getAlphaPath(p
));
663 public final String
getMasksFolder() {
664 if (null == dir_masks
) createMasksFolder();
668 synchronized private final void createMasksFolder() {
669 if (null == dir_masks
) dir_masks
= getUNUIdFolder() + "trakem2.masks/";
670 final File f
= new File(dir_masks
);
671 if (f
.exists() && f
.isDirectory()) return;
674 } catch (Exception e
) {
679 /** Remove the file containing the given Patch's alpha mask. */
680 public final boolean removeAlphaMask(final Patch p
) {
682 File f
= new File(getAlphaPath(p
));
687 } catch (Exception e
) {
693 /** Loaded in full from XML file */
694 public Object
[] fetchLabel(DLabel label
) {
698 /** Loads and returns the original image, which is not cached, or returns null if it's not different than the working image. */
699 synchronized public ImagePlus
fetchOriginal(final Patch patch
) {
700 String original_path
= patch
.getOriginalPath();
701 if (null == original_path
) return null;
702 // else, reserve memory and open it:
703 long n_bytes
= estimateImageFileSize(patch
, 0);
705 synchronized (db_lock
) {
707 max_memory
-= n_bytes
;
711 return openImage(original_path
);
712 } catch (Throwable t
) {
715 synchronized (db_lock
) {
717 max_memory
+= n_bytes
;
724 public void prepare(Layer layer
) {
725 //Utils.log2("FSLoader.prepare(Layer): not implemented.");
726 super.prepare(layer
);
729 /* GENERIC, from DBObject calls. Records the id of the object in the HashMap ht_dbo.
730 * Always returns true. Does not check if another object has the same id.
732 public boolean addToDatabase(final DBObject ob
) {
733 synchronized (db_lock
) {
736 final long id
= ob
.getId();
745 public boolean updateInDatabase(final DBObject ob
, final String key
) {
746 // Should only be GUI-driven
749 if (ob
.getClass() == Patch
.class) {
751 if (key
.equals("tiff_working")) return null != setImageFile(p
, fetchImagePlus(p
));
756 public boolean removeFromDatabase(final DBObject ob
) {
757 synchronized (db_lock
) {
760 // remove from the hashtable
761 final long loid
= ob
.getId();
762 Utils
.log2("removing " + Project
.getName(ob
.getClass()) + " " + ob
);
763 if (ob
.getClass() == Patch
.class) {
764 // STRATEGY change: images are not owned by the FSLoader.
766 if (!ob
.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p
);
767 ht_paths
.remove(p
.getId()); // after removeMipMaps !
768 mawts
.removeAndFlush(loid
);
769 final ImagePlus imp
= imps
.remove(loid
);
771 if (imp
.getStackSize() > 1) {
772 if (null == imp
.getProcessor()) {}
773 else if (null == imp
.getProcessor().getPixels()) {}
774 else Loader
.flush(imp
); // only once
779 cannot_regenerate
.remove(p
);
781 flushMipMaps(p
.getId()); // locks on its own
782 touched_mipmaps
.remove(p
);
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
) {
884 * Never used. Was this planned to be what we do no with DBObject.getUniqueId()?
886 private final String
makeFileTitle(final Patch p
) {
887 String title
= p
.getTitle();
888 if (null == title
) return "image-" + p
.getId();
889 title
= asSafePath(title
);
890 if (0 == title
.length()) return "image-" + p
.getId();
894 /** Associate patch with imp, and all slices as well if any. */
895 private void cacheAll(final Patch p
, final ImagePlus imp
) {
897 for (Patch pa
: p
.getStackPatches()) {
905 /** For the Patch and for any associated slices if the patch is part of a stack. */
906 private void updatePaths(final Patch patch
, final String path
, final boolean is_stack
) {
907 synchronized (db_lock
) {
910 // ensure the old path is cached in the Patch, to get set as the original if there is no original.
912 for (Patch p
: patch
.getStackPatches()) {
913 long pid
= p
.getId();
914 String str
= ht_paths
.get(pid
);
915 int isl
= str
.lastIndexOf("-----#slice=");
916 updatePatchPath(p
, path
+ str
.substring(isl
));
919 Utils
.log2("path to set: " + path
);
920 Utils
.log2("path before: " + ht_paths
.get(patch
.getId()));
921 updatePatchPath(patch
, path
);
922 Utils
.log2("path after: " + ht_paths
.get(patch
.getId()));
924 } catch (Throwable e
) {
932 /** With slice info appended at the end; only if it exists, otherwise null. */
933 public String
getAbsolutePath(final Patch patch
) {
934 String abs_path
= patch
.getCurrentPath();
935 if (null != abs_path
) return abs_path
;
936 // else, compute, set and return it:
937 String path
= ht_paths
.get(patch
.getId());
938 if (null == path
) return null;
939 // substract slice info if there
940 int i_sl
= path
.lastIndexOf("-----#slice=");
943 slice
= path
.substring(i_sl
);
944 path
= path
.substring(0, i_sl
);
946 if (isRelativePath(path
)) {
947 // path is relative: preprend the parent folder of the xml file
948 path
= getParentFolder() + path
;
949 if (!isURL(path
) && !new File(path
).exists()) {
950 Utils
.log("Path for patch " + patch
+ " does not exist: " + path
);
953 // else assume that it exists
955 // reappend slice info if existent
956 if (null != slice
) path
+= slice
;
958 patch
.cacheCurrentPath(path
);
962 public final String
getAbsoluteFilePath(final Patch p
) {
963 final String path
= getAbsolutePath(p
);
964 if (null == path
) return null;
965 final int i
= path
.lastIndexOf("----#slice");
966 return -1 == i ? path
967 : path
.substring(0, i
);
970 public static final boolean isURL(final String path
) {
971 return null != path
&& 0 == path
.indexOf("http://");
974 static public final Pattern ABS_PATH
= Pattern
.compile("^[a-zA-Z]*:/.*$|^/.*$|[a-zA-Z]:.*$");
976 public static final boolean isRelativePath(final String path
) {
977 return ! ABS_PATH
.matcher(path
).matches();
980 /** All backslashes are converted to slashes to avoid havoc in MSWindows. */
981 public void addedPatchFrom(String path
, final Patch patch
) {
983 Utils
.log("Null path for patch: " + patch
);
986 updatePatchPath(patch
, path
);
989 /** This method has the exclusivity in calling ht_paths.put, because it ensures the path won't have escape characters. */
990 private final void updatePatchPath(final Patch patch
, String path
) { // reversed order in purpose, relative to addedPatchFrom
991 // avoid W1nd0ws nightmares
992 path
= path
.replace('\\', '/'); // replacing with chars, in place
993 // remove double slashes that a user may have slipped in
994 final int start
= isURL(path
) ?
6 : (IJ
.isWindows() ?
3 : 1);
995 while (-1 != path
.indexOf("//", start
)) {
996 // avoid the potential C:// of windows and the starting // of a samba network
997 path
= path
.substring(0, start
) + path
.substring(start
).replace("//", "/");
999 // cache path as absolute
1000 patch
.cacheCurrentPath(isRelativePath(path
) ?
getParentFolder() + path
: path
);
1001 // if path is absolute, try to make it relative
1002 //Utils.log2("path was: " + path);
1003 path
= makeRelativePath(path
);
1005 ht_paths
.put(patch
.getId(), path
);
1006 //Utils.log2("Updated patch path " + ht_paths.get(patch.getId()) + " for patch " + patch);
1009 /** Takes a String and returns a copy with the following conversions: / to -, space to _, and \ to -. */
1010 public String
asSafePath(final String name
) {
1011 return name
.trim().replace('/', '-').replace(' ', '_').replace('\\','-');
1014 /** 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
) {
1016 String result
= null;
1017 if (null == project_file_path
) {
1018 String xml_path
= super.saveAs(project
, null, false);
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
, false);
1029 if (null != result
) {
1030 Utils
.logAll(Utils
.now() + " Saved " + project
);
1031 touched_mipmaps
.clear();
1036 public String
saveAs(Project project
) {
1037 String path
= super.saveAs(project
, null, false);
1039 // update the xml path to point to the new one
1040 this.project_file_path
= path
;
1041 Utils
.log2("After saveAs, new xml path is: " + path
);
1043 ControlWindow
.updateTitle(project
);
1047 /** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
1048 public String
saveAs(final String path
, final boolean overwrite
) {
1050 Utils
.log("Cannot save on null path.");
1053 String path2
= path
;
1054 if (!path2
.endsWith(".xml")) path2
+= ".xml";
1055 File fxml
= new File(path2
);
1056 if (!fxml
.canWrite()) {
1057 // write to storage folder instead
1058 String path3
= path2
;
1059 path2
= getStorageFolder() + fxml
.getName();
1060 Utils
.logAll("WARNING can't write to " + path3
+ "\n --> will write instead to " + path2
);
1061 fxml
= new File(path2
);
1065 while (fxml
.exists()) {
1066 String parent
= fxml
.getParent().replace('\\','/');
1067 if (!parent
.endsWith("/")) parent
+= "/";
1068 String name
= fxml
.getName();
1069 name
= name
.substring(0, name
.length() - 4);
1070 path2
= parent
+ name
+ "-" + i
+ ".xml";
1071 fxml
= new File(path2
);
1075 Project project
= Project
.findProject(this);
1076 path2
= super.saveAs(project
, path2
, false);
1077 if (null != path2
) {
1078 project_file_path
= path2
;
1079 Utils
.logAll("After saveAs, new xml path is: " + path2
);
1080 ControlWindow
.updateTitle(project
);
1081 touched_mipmaps
.clear();
1086 /** Returns the stored path for the given Patch image, which may be relative and may contain slice information appended.*/
1087 public String
getPath(final Patch patch
) {
1088 return ht_paths
.get(patch
.getId());
1091 /** 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. */
1092 private String
makeRelativePath(String path
) {
1093 if (null == project_file_path
) {
1100 // fix W1nd0ws paths
1101 path
= path
.replace('\\', '/'); // char-based, no parsing problems
1103 String slice
= null;
1104 int isl
= path
.lastIndexOf("-----#slice");
1106 slice
= path
.substring(isl
);
1107 path
= path
.substring(0, isl
);
1110 if (isRelativePath(path
)) {
1112 if (-1 != isl
) path
+= slice
;
1115 // the long and verbose way, to be cross-platform. Should work with URLs just the same.
1116 String xdir
= new File(project_file_path
).getParentFile().getAbsolutePath();
1117 if (!xdir
.endsWith("/")) xdir
+= "/";
1118 if (IJ
.isWindows()) {
1119 xdir
= xdir
.replace('\\', '/');
1120 path
= path
.replace('\\', '/');
1122 if (path
.startsWith(xdir
)) {
1123 path
= path
.substring(xdir
.length());
1125 if (-1 != isl
) path
+= slice
;
1126 //Utils.log("made relative path: " + path);
1130 /** Adds a "Save" and "Save as" menu items. */
1131 public void setupMenuItems(final JMenu menu
, final Project project
) {
1132 ActionListener listener
= new ActionListener() {
1133 public void actionPerformed(ActionEvent ae
) {
1134 String command
= ae
.getActionCommand();
1135 if (command
.equals("Save")) {
1137 } else if (command
.equals("Save as...")) {
1143 item
= new JMenuItem("Save"); item
.addActionListener(listener
); menu
.add(item
);
1144 item
.setAccelerator(KeyStroke
.getKeyStroke(KeyEvent
.VK_S
, 0, true));
1145 item
= new JMenuItem("Save as..."); item
.addActionListener(listener
); menu
.add(item
);
1148 /** Returns the last Patch. */
1149 protected Patch
importStackAsPatches(final Project project
, final Layer first_layer
, final int x
, final int y
, final ImagePlus imp_stack
, final boolean as_copy
, final String filepath
) {
1150 Utils
.log2("FSLoader.importStackAsPatches filepath=" + filepath
);
1151 String target_dir
= null;
1153 DirectoryChooser dc
= new DirectoryChooser("Folder to save images");
1154 target_dir
= dc
.getDirectory();
1155 if (null == target_dir
) return null; // user canceled dialog
1156 if (target_dir
.length() -1 != target_dir
.lastIndexOf('/')) {
1161 final boolean virtual
= imp_stack
.getStack().isVirtual();
1163 int pos_x
= Integer
.MAX_VALUE
!= x ? x
: (int)first_layer
.getLayerWidth()/2 - imp_stack
.getWidth()/2;
1164 int pos_y
= Integer
.MAX_VALUE
!= y ? y
: (int)first_layer
.getLayerHeight()/2 - imp_stack
.getHeight()/2;
1165 final double thickness
= first_layer
.getThickness();
1166 final String title
= Utils
.removeExtension(imp_stack
.getTitle()).replace(' ', '_');
1167 Utils
.showProgress(0);
1168 Patch previous_patch
= null;
1169 final int n
= imp_stack
.getStackSize();
1170 for (int i
=1; i
<=n
; i
++) {
1171 Layer layer
= first_layer
;
1172 double z
= first_layer
.getZ() + (i
-1) * thickness
;
1173 if (i
> 1) layer
= first_layer
.getParent().getLayer(z
, thickness
, true); // will create new layer if not found
1174 if (null == layer
) {
1175 Utils
.log("Display.importStack: could not create new layers.");
1178 String patch_path
= null;
1180 ImagePlus imp_patch_i
= null;
1181 if (virtual
) { // because we love inefficiency, every time all this is done again
1182 VirtualStack vs
= (VirtualStack
)imp_stack
.getStack();
1183 String vs_dir
= vs
.getDirectory().replace('\\', '/');
1184 if (!vs_dir
.endsWith("/")) vs_dir
+= "/";
1185 String iname
= vs
.getFileName(i
);
1186 patch_path
= vs_dir
+ iname
;
1187 Utils
.log2("virtual stack: patch path is " + patch_path
);
1189 Utils
.log2(i
+ " : " + patch_path
);
1190 imp_patch_i
= openImage(patch_path
);
1192 ImageProcessor ip
= imp_stack
.getStack().getProcessor(i
);
1193 if (as_copy
) ip
= ip
.duplicate();
1194 imp_patch_i
= new ImagePlus(title
+ "__slice=" + i
, ip
);
1196 preProcess(imp_patch_i
);
1198 String label
= imp_stack
.getStack().getSliceLabel(i
);
1199 if (null == label
) label
= "";
1202 patch_path
= target_dir
+ imp_patch_i
.getTitle() + ".zip";
1203 ini
.trakem2
.io
.ImageSaver
.saveAsZip(imp_patch_i
, patch_path
);
1204 patch
= new Patch(project
, label
+ " " + title
+ " " + i
, pos_x
, pos_y
, imp_patch_i
);
1205 } else if (virtual
) {
1206 patch
= new Patch(project
, label
, pos_x
, pos_y
, imp_patch_i
);
1208 patch_path
= filepath
+ "-----#slice=" + i
;
1209 //Utils.log2("path is "+ patch_path);
1210 final AffineTransform atp
= new AffineTransform();
1211 atp
.translate(pos_x
, pos_y
);
1212 patch
= new Patch(project
, getNextId(), label
+ " " + title
+ " " + i
, imp_stack
.getWidth(), imp_stack
.getHeight(), imp_stack
.getType(), false, imp_stack
.getProcessor().getMin(), imp_stack
.getProcessor().getMax(), atp
);
1213 patch
.addToDatabase();
1214 //Utils.log2("type is " + imp_stack.getType());
1216 Utils
.log2("B: " + i
+ " : " + patch_path
);
1217 addedPatchFrom(patch_path
, patch
);
1218 if (!as_copy
&& !virtual
) {
1219 if (virtual
) cache(patch
, imp_patch_i
); // each slice separately
1220 else cache(patch
, imp_stack
); // uses the entire stack, shared among all Patch instances
1222 if (isMipMapsEnabled()) generateMipMaps(patch
);
1223 if (null != previous_patch
) patch
.link(previous_patch
);
1225 previous_patch
= patch
;
1226 Utils
.showProgress(i
* (1.0 / n
));
1228 Utils
.showProgress(1.0);
1230 // update calibration
1233 // return the last patch
1234 return previous_patch
;
1237 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1238 public void parseXMLOptions(final HashMap ht_attributes
) {
1239 Object ob
= ht_attributes
.remove("preprocessor");
1241 setPreprocessor((String
)ob
);
1243 // Adding some logic to support old projects which lack a storage folder and a mipmaps folder
1244 // and also to prevent errors such as those created when manualy tinkering with the XML file
1245 // or renaming directories, etc.
1246 ob
= ht_attributes
.remove("storage_folder");
1248 String sf
= ((String
)ob
).replace('\\', '/');
1249 if (isRelativePath(sf
)) {
1250 sf
= getParentFolder() + sf
;
1254 Utils
.log2("Can't have an URL as the path of a storage folder.");
1256 File f
= new File(sf
);
1257 if (f
.exists() && f
.isDirectory()) {
1258 this.dir_storage
= sf
;
1260 Utils
.log2("storage_folder was not found or is invalid: " + ob
);
1264 if (null == this.dir_storage
) {
1265 // select the directory where the xml file lives.
1266 this.dir_storage
= getParentFolder();
1267 if (null == this.dir_storage
|| isURL(this.dir_storage
)) this.dir_storage
= null;
1268 if (null == this.dir_storage
&& ControlWindow
.isGUIEnabled()) {
1269 Utils
.log2("Asking user for a storage folder in a dialog."); // tip for headless runners whose program gets "stuck"
1270 DirectoryChooser dc
= new DirectoryChooser("REQUIRED: select a storage folder");
1271 this.dir_storage
= dc
.getDirectory();
1273 if (null == this.dir_storage
) {
1274 IJ
.showMessage("TrakEM2 requires a storage folder.\nTemporarily your home directory will be used.");
1275 this.dir_storage
= System
.getProperty("user.home").replace('\\', '/');
1279 if (null != this.dir_storage
&& !this.dir_storage
.endsWith("/")) this.dir_storage
+= "/";
1280 Utils
.log2("storage folder is " + this.dir_storage
);
1282 ob
= ht_attributes
.remove("mipmaps_folder");
1284 String mf
= ((String
)ob
).replace('\\', '/');
1285 if (isRelativePath(mf
)) {
1286 mf
= getParentFolder() + mf
;
1289 this.dir_mipmaps
= mf
;
1290 // TODO must disable input somehow, so that images are not edited.
1292 File f
= new File(mf
);
1293 if (f
.exists() && f
.isDirectory()) {
1294 this.dir_mipmaps
= mf
;
1296 Utils
.log2("mipmaps_folder was not found or is invalid: " + ob
);
1301 // parse the unuid before attempting to create any folders
1302 this.unuid
= (String
) ht_attributes
.remove("unuid");
1304 if (null == this.dir_mipmaps
) {
1305 // create a new one inside the dir_storage, which can't be null
1306 createMipMapsDir(dir_storage
);
1307 if (null != this.dir_mipmaps
&& ControlWindow
.isGUIEnabled() && null != IJ
.getInstance()) {
1308 askAndExecMipmapRegeneration(null);
1312 if (null != this.dir_mipmaps
&& !this.dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
1313 Utils
.log2("mipmaps folder is " + this.dir_mipmaps
);
1315 if (null == unuid
) {
1316 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.");
1317 if (ControlWindow
.isGUIEnabled()) {
1318 obtainUNUIdFolder();
1319 if (null != this.unuid
) return; // one was selected, which was hopefully valid
1321 Utils
.log2("Creating unuid for project " + this);
1322 this.unuid
= createUNUId(dir_storage
);
1323 fixStorageFolders();
1324 Utils
.log2("Now mipmaps folder is " + this.dir_mipmaps
);
1325 if (null != dir_masks
) Utils
.log2("Now masks folder is " + this.dir_masks
);
1329 private void askAndExecMipmapRegeneration(final String msg
) {
1330 Utils
.log2("Asking user Yes/No to generate mipmaps on the background."); // tip for headless runners whose program gets "stuck"
1331 YesNoDialog yn
= new YesNoDialog(IJ
.getInstance(), "Generate mipmaps", (null != msg ? msg
+ "\n" : "") + "Generate mipmaps in the background for all images?");
1332 if (yn
.yesPressed()) {
1333 final Loader lo
= this;
1337 // wait while parsing the rest of the XML file
1338 while (!v_loaders
.contains(lo
)) {
1341 Project pj
= Project
.findProject(lo
);
1342 // Submit a task for each Patch:
1343 for (final Displayable patch
: pj
.getRootLayerSet().getDisplayables(Patch
.class)) {
1344 ((FSLoader
)lo
).regenerateMipMaps((Patch
)patch
);
1346 } catch (Exception e
) {}
1352 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1353 public void insertXMLOptions(StringBuffer sb_body
, String indent
) {
1354 sb_body
.append(indent
).append("unuid=\"").append(unuid
).append("\"\n");
1355 if (null != preprocessor
) sb_body
.append(indent
).append("preprocessor=\"").append(preprocessor
).append("\"\n");
1356 if (null != dir_mipmaps
) sb_body
.append(indent
).append("mipmaps_folder=\"").append(makeRelativePath(dir_mipmaps
)).append("\"\n");
1357 if (null != dir_storage
) sb_body
.append(indent
).append("storage_folder=\"").append(makeRelativePath(dir_storage
)).append("\"\n");
1360 /** Return the path to the folder containing the project XML file. */
1361 private final String
getParentFolder() {
1362 return this.project_file_path
.substring(0, this.project_file_path
.lastIndexOf('/')+1);
1365 /* ************** MIPMAPS **********************/
1367 /** Returns the path to the directory hosting the file image pyramids. */
1368 public String
getMipMapsFolder() {
1374 static private IndexColorModel thresh_cm = null;
1376 static private final IndexColorModel getThresholdLUT() {
1377 if (null == thresh_cm) {
1378 // An array of all black pixels (value 0) except at 255, which is white (value 255).
1379 final byte[] c = new byte[256];
1381 thresh_cm = new IndexColorModel(8, 256, c, c, c);
1387 /** Returns the array of pixels, whose type depends on the bi.getType(); for example, for a BufferedImage.TYPE_BYTE_INDEXED, returns a byte[]. */
1388 static public final Object
grabPixels(final BufferedImage bi
) {
1389 final PixelGrabber pg
= new PixelGrabber(bi
, 0, 0, bi
.getWidth(), bi
.getHeight(), false);
1392 return pg
.getPixels();
1393 } catch (InterruptedException e
) {
1399 private final BufferedImage
createCroppedAlpha(final BufferedImage alpha
, final BufferedImage outside
) {
1400 if (null == outside
) return alpha
;
1402 final int width
= outside
.getWidth();
1403 final int height
= outside
.getHeight();
1405 // Create an outside image, thresholded: only pixels of 255 remain as 255, the rest is set to 0.
1406 /* // DOESN'T work: creates a mask with "black" as 254 (???), and white 255 (correct).
1407 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, getThresholdLUT());
1408 thresholded.createGraphics().drawImage(outside, 0, 0, null);
1411 // So, instead: grab the pixels, fix them manually
1412 // The cast to byte[] works because "outside" and "alpha" are TYPE_BYTE_INDEXED.
1413 final byte[] o
= (byte[])grabPixels(outside
);
1414 if (null == o
) return null;
1415 final byte[] a
= null == alpha ? o
: (byte[])grabPixels(alpha
);
1417 // Set each non-255 pixel in outside to 0 in alpha:
1418 for (int i
=0; i
<o
.length
; i
++) {
1419 if ( (o
[i
]&0xff) < 255) a
[i
] = 0;
1422 // Put the pixels back into an image:
1423 final BufferedImage thresholded
= new BufferedImage(width
, height
, BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1424 thresholded
.getRaster().setDataElements(0, 0, width
, height
, a
);
1429 static public final BufferedImage
convertToBufferedImage(final ByteProcessor bp
) {
1430 bp
.setMinAndMax(0, 255);
1431 final Image img
= bp
.createImage();
1432 if (img
instanceof BufferedImage
) return (BufferedImage
)img
;
1434 final BufferedImage bi
= new BufferedImage(bp
.getWidth(), bp
.getHeight(), BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1435 bi
.createGraphics().drawImage(bi
, 0, 0, null);
1439 /** Scale a BufferedImage.TYPE_BYTE_INDEXED into another of the same type but dimensions target_width,target_height. */
1440 static private final BufferedImage
scaleAndFlush(final Image img
, final int target_width
, final int target_height
, final boolean area_averaging
, final Object interpolation_hint
) {
1441 final BufferedImage bi
= new BufferedImage(target_width
, target_height
, BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1442 if (area_averaging
) {
1443 bi
.createGraphics().drawImage(img
.getScaledInstance(target_width
, target_height
, Image
.SCALE_AREA_AVERAGING
), 0, 0, null);
1445 final Graphics2D g
= bi
.createGraphics();
1446 g
.setRenderingHint(RenderingHints
.KEY_INTERPOLATION
, interpolation_hint
);
1447 g
.drawImage(img
, 0, 0, target_width
, target_height
, null); // draws it scaled to target area w*h
1449 // Release native resources
1455 /** Image to BufferedImage. Can be used for hardware-accelerated resizing, since the whole awt is painted to a target w,h area in the returned new BufferedImage. Does not accept LUT images: only ARGB or GRAY. */
1456 private final BufferedImage
[] IToBI(final Image awt
, final int w
, final int h
, final Object interpolation_hint
, final BufferedImage alpha
, final BufferedImage outside
) {
1458 final boolean area_averaging
= interpolation_hint
.getClass() == Integer
.class && Loader
.AREA_AVERAGING
== ((Integer
)interpolation_hint
).intValue();
1459 final boolean must_scale
= (w
!= awt
.getWidth(null) || h
!= awt
.getHeight(null));
1461 if (null != alpha
|| null != outside
) bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_INT_ARGB
);
1462 else bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_BYTE_GRAY
);
1463 final Graphics2D g
= bi
.createGraphics();
1464 if (area_averaging
) {
1465 final Image img
= awt
.getScaledInstance(w
, h
, Image
.SCALE_AREA_AVERAGING
); // Creates ALWAYS an RGB image, so must repaint back to a single-channel image, avoiding unnecessary blow up of memory.
1466 g
.drawImage(img
, 0, 0, null);
1468 g
.setRenderingHint(RenderingHints
.KEY_INTERPOLATION
, interpolation_hint
);
1469 g
.drawImage(awt
, 0, 0, w
, h
, null); // draws it scaled
1471 BufferedImage ba
= alpha
;
1472 BufferedImage bo
= outside
;
1473 if (null != alpha
&& must_scale
) {
1474 ba
= scaleAndFlush(alpha
, w
, h
, area_averaging
, interpolation_hint
);
1476 if (null != outside
&& must_scale
) {
1477 bo
= scaleAndFlush(outside
, w
, h
, area_averaging
, interpolation_hint
);
1480 BufferedImage the_alpha
= ba
;
1481 if (null != alpha
) {
1482 if (null != outside
) {
1483 the_alpha
= createCroppedAlpha(ba
, bo
);
1485 } else if (null != outside
) {
1486 the_alpha
= createCroppedAlpha(null, bo
);
1488 if (null != the_alpha
) {
1489 bi
.getAlphaRaster().setRect(the_alpha
.getRaster());
1490 //bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])new ImagePlus("", the_alpha).getProcessor().convertToFloat().getPixels());
1494 //Utils.log2("bi is: " + bi.getType() + " BufferedImage.TYPE_INT_ARGB=" + BufferedImage.TYPE_INT_ARGB);
1497 FloatProcessor fp_alpha = null;
1498 fp_alpha = (FloatProcessor) new ByteProcessor(ba).convertToFloat();
1499 // Set all non-white pixels to zero (eliminate shadowy border caused by interpolation)
1500 final float[] pix = (float[])fp_alpha.getPixels();
1501 for (int i=0; i<pix.length; i++)
1502 if (Math.abs(pix[i] - 255) > 0.001f) pix[i] = 0;
1503 bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])fp_alpha.getPixels());
1506 return new BufferedImage
[]{bi
, ba
, bo
};
1509 private final Object
getHint(final int mode
) {
1511 case Loader
.BICUBIC
:
1512 return RenderingHints
.VALUE_INTERPOLATION_BICUBIC
;
1513 case Loader
.BILINEAR
:
1514 return RenderingHints
.VALUE_INTERPOLATION_BILINEAR
;
1515 case Loader
.AREA_AVERAGING
:
1516 return new Integer(mode
);
1517 case Loader
.NEAREST_NEIGHBOR
:
1519 return RenderingHints
.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
;
1523 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1524 static final private byte[] gaussianBlurResizeInHalf(final FloatProcessorT2 source
, final int source_width
, final int source_height
, final int target_width
, final int target_height
) {
1525 source
.setPixels(source_width
, source_height
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])source
.getPixels(), source_width
, source_height
), 0.75f
).data
);
1526 source
.resizeInPlace(target_width
, target_height
);
1527 return (byte[])source
.convertToByte(false).getPixels(); // no scaling
1530 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1531 static final private byte[] meanResizeInHalf(final FloatProcessorT2 source
, final int sourceWidth
, final int sourceHeight
, final int targetWidth
, final int targetHeight
) {
1532 final float[] sourceData
= source
.getFloatPixels();
1533 final float[] targetData
= new float[targetWidth
* targetHeight
];
1535 for (int r
= 0; r
< targetData
.length
; r
+= targetWidth
) {
1537 for (int x
= 0; x
< targetWidth
; ++x
)
1538 targetData
[r
+ x
] = sourceData
[rs
+ ++xs
] + sourceData
[rs
+ ++xs
];
1541 for (int x
= 0; x
< targetWidth
; ++x
) {
1542 targetData
[r
+ x
] += sourceData
[rs
+ ++xs
] + sourceData
[rs
+ ++xs
];
1543 targetData
[r
+ x
] /= 4;
1547 source
.setPixels(targetWidth
, targetHeight
, targetData
);
1548 return (byte[])source
.convertToByte(false).getPixels();
1551 /** Queue/unqueue for mipmap removal on shutdown without saving. */
1552 public void queueForMipmapRemoval(final Patch p
, boolean yes
) {
1553 if (yes
) touched_mipmaps
.add(p
);
1554 else touched_mipmaps
.remove(p
);
1557 /** Given an image and its source file name (without directory prepended), generate
1558 * a pyramid of images until reaching an image not smaller than 32x32 pixels.<br />
1559 * Such images are stored as jpeg 85% quality in a folder named trakem2.mipmaps.<br />
1560 * The Patch id and a ".jpg" extension will be appended to the filename in all cases.<br />
1561 * Any equally named files will be overwritten.
1563 public boolean generateMipMaps(final Patch patch
) {
1564 return generateMipMaps(patch
, true);
1566 /** The boolean flag is because the submission to the ExecutorService is too fast, and I need to prevent it before it can ever add duplicates to the queue, so I need to ask that question before invoking this method. */
1567 private boolean generateMipMaps(final Patch patch
, final boolean check_if_already_being_done
) {
1568 Utils
.log2("mipmaps for " + patch
);
1569 final String path
= getAbsolutePath(patch
);
1571 Utils
.log2("generateMipMaps: cannot find path for Patch " + patch
);
1572 cannot_regenerate
.add(patch
);
1575 if (hs_unloadable
.contains(patch
)) {
1576 FilePathRepair
.add(patch
);
1579 synchronized (gm_lock
) {
1582 if (null == dir_mipmaps
) createMipMapsDir(null);
1583 if (null == dir_mipmaps
|| isURL(dir_mipmaps
)) return false;
1584 if (check_if_already_being_done
&& hs_regenerating_mipmaps
.contains(patch
)) {
1585 // already being done
1586 Utils
.log2("Already being done: " + patch
);
1589 hs_regenerating_mipmaps
.add(patch
);
1590 } catch (Exception e
) {
1597 /** Record Patch as modified */
1598 touched_mipmaps
.add(patch
);
1600 /** Remove serialized features, if any */
1601 removeSerializedFeatures(patch
);
1603 /** Remove serialized pointmatches, if any */
1604 removeSerializedPointMatches(patch
);
1606 String srmode
= patch
.getProject().getProperty("image_resizing_mode");
1607 int resizing_mode
= GAUSSIAN
;
1608 if (null != srmode
) resizing_mode
= Loader
.getMode(srmode
);
1612 // 1 - Ask the Patch to apply a coordinate transform, or rather, create a function that gets the coordinate transform from the Patch and applies it to the 'ip'.
1613 // 2 - Then (1) should return both the transformed image and the alpha mask
1616 ByteProcessor alpha_mask
= null;
1617 ByteProcessor outside_mask
= null;
1618 final boolean coordinate_transformed
;
1619 int type
= patch
.getType();
1621 // Obtain an image which may be coordinate-transformed, and an alpha mask.
1622 Patch
.PatchImage pai
= patch
.createTransformedImage();
1624 Utils
.log("Can't regenerate mipmaps for patch " + patch
);
1625 cannot_regenerate
.add(patch
);
1629 alpha_mask
= pai
.mask
; // can be null
1630 outside_mask
= pai
.outside
; // can be null
1631 coordinate_transformed
= pai
.coordinate_transformed
;
1635 //final String filename = new StringBuffer(new File(path).getName()).append('.').append(patch.getId()).append(".jpg").toString();
1637 final String filename
= createMipMapRelPath(patch
);
1639 int w
= ip
.getWidth();
1640 int h
= ip
.getHeight();
1642 // sigma = sqrt(2^level - 0.5^2)
1643 // where 0.5 is the estimated sigma for a full-scale image
1644 // which means sigma = 0.75 for the full-scale image (has level 0)
1645 // prepare a 0.75 sigma image from the original
1646 ColorModel cm
= ip
.getColorModel();
1647 int k
= 0; // the scale level. Proper scale is: 1 / pow(2, k)
1648 // but since we scale 50% relative the previous, it's always 0.75
1650 double min
= patch
.getMin(),
1651 max
= patch
.getMax();
1652 // Fix improper min,max values
1653 // (The -1,-1 are flags really for "not set")
1654 if (-1 == min
&& -1 == max
) {
1656 case ImagePlus
.COLOR_RGB
:
1657 case ImagePlus
.COLOR_256
:
1658 case ImagePlus
.GRAY8
:
1659 patch
.setMinAndMax(0, 255);
1661 // Find and flow through to default:
1662 case ImagePlus
.GRAY16
:
1663 ((ij
.process
.ShortProcessor
)ip
).findMinAndMax();
1664 patch
.setMinAndMax(ip
.getMin(), ip
.getMax());
1666 case ImagePlus
.GRAY32
:
1667 ((FloatProcessor
)ip
).findMinAndMax();
1668 patch
.setMinAndMax(ip
.getMin(), ip
.getMax());
1671 min
= patch
.getMin(); // may have changed
1672 max
= patch
.getMax();
1675 // Set for the level 0 image, which is a duplicate of the one on the cache in any case
1676 ip
.setMinAndMax(min
, max
);
1679 // Proper support for LUT images: treat them as RGB
1680 if (ip
.isColorLut()) {
1681 ip
= ip
.convertToRGB();
1683 type
= ImagePlus
.COLOR_RGB
;
1686 if (ImagePlus
.COLOR_RGB
== type
) {
1687 // TODO releaseToFit proper
1688 releaseToFit(w
* h
* 4 * 5);
1689 final ColorProcessor cp
= (ColorProcessor
)ip
;
1690 final FloatProcessorT2 red
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(0, red
);
1691 final FloatProcessorT2 green
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(1, green
);
1692 final FloatProcessorT2 blue
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(2, blue
);
1693 FloatProcessorT2 alpha
;
1694 final FloatProcessorT2 outside
;
1695 if (null != alpha_mask
) {
1696 alpha
= new FloatProcessorT2((FloatProcessor
)alpha_mask
.convertToFloat());
1700 if (null != outside_mask
) {
1701 outside
= new FloatProcessorT2((FloatProcessor
)outside_mask
.convertToFloat());
1702 if ( null == alpha
) {
1704 alpha_mask
= outside_mask
;
1710 // sw,sh are the dimensions of the image to blur
1711 // w,h are the dimensions to scale the blurred image to
1715 final String target_dir0
= getLevelDir(dir_mipmaps
, 0);
1716 // No alpha channel:
1717 // - use gaussian resizing
1718 // - use standard ImageJ java.awt.Image creation
1720 if (Thread
.currentThread().isInterrupted()) return false;
1722 // Generate level 0 first:
1723 // TODO Add alpha information into the int[] pixel array or make the image visible some ohter way
1724 if (!(null == alpha ? ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(cp
, target_dir0
+ filename
, 0.85f
, false)
1725 : ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(createARGBImage(w
, h
, embedAlpha((int[])cp
.getPixels(), (byte[])alpha_mask
.getPixels(), null == outside ?
null : (byte[])outside_mask
.getPixels())), target_dir0
+ filename
, 0.85f
))) {
1726 cannot_regenerate
.add(patch
);
1729 if (Thread
.currentThread().isInterrupted()) return false;
1730 // 1 - Prepare values for the next scaled image
1736 // 2 - Check that the target folder for the desired scale exists
1737 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1738 if (null == target_dir
) continue;
1739 // 3 - Blur the previous image to 0.75 sigma, and scale it
1740 final byte[] r
= gaussianBlurResizeInHalf(red
, sw
, sh
, w
, h
); // will resize 'red' FloatProcessor in place.
1741 final byte[] g
= gaussianBlurResizeInHalf(green
, sw
, sh
, w
, h
); // idem
1742 final byte[] b
= gaussianBlurResizeInHalf(blue
, sw
, sh
, w
, h
); // idem
1743 final byte[] a
= null == alpha ?
null : gaussianBlurResizeInHalf(alpha
, sw
, sh
, w
, h
); // idem
1744 if ( null != outside
) {
1746 if (alpha
!= outside
)
1747 o
= gaussianBlurResizeInHalf(outside
, sw
, sh
, w
, h
); // idem
1750 // Remove all not completely inside pixels from the alphamask
1751 // If there was no alpha mask, alpha is the outside itself
1752 for (int i
=0; i
<o
.length
; i
++) {
1753 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;
1757 // 4 - Compose ColorProcessor
1758 final int[] pix
= new int[w
* h
];
1759 if (null == alpha
) {
1760 for (int i
=0; i
<pix
.length
; i
++) {
1761 pix
[i
] = 0xff000000 | ((r
[i
]&0xff)<<16) | ((g
[i
]&0xff)<<8) | (b
[i
]&0xff);
1763 final ColorProcessor cp2
= new ColorProcessor(w
, h
, pix
);
1765 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(cp2
, target_dir
+ filename
, 0.85f
, false)) {
1766 cannot_regenerate
.add(patch
);
1770 // LIKELY no need to set alpha raster later in createARGBImage ... TODO
1771 for (int i
=0; i
<pix
.length
; i
++) {
1772 pix
[i
] = ((a
[i
]&0xff)<<24) | ((r
[i
]&0xff)<<16) | ((g
[i
]&0xff)<<8) | (b
[i
]&0xff);
1774 final BufferedImage bi_save
= createARGBImage(w
, h
, pix
);
1775 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi_save
, target_dir
+ filename
, 0.85f
)) {
1776 cannot_regenerate
.add(patch
);
1782 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
1786 releaseToFit(w
* h
* 4 * 5);
1787 final boolean as_grey
= !ip
.isColorLut();
1788 if (as_grey
&& null == cm
) {
1792 if (Thread
.currentThread().isInterrupted()) return false;
1794 if (Loader
.GAUSSIAN
== resizing_mode
) {
1795 FloatProcessor fp
= (FloatProcessor
) ip
.convertToFloat();
1798 FloatProcessor alpha
;
1799 FloatProcessor outside
;
1800 if (null != alpha_mask
) {
1801 alpha
= new FloatProcessorT2((FloatProcessor
)alpha_mask
.convertToFloat());
1805 if (null != outside_mask
) {
1806 outside
= new FloatProcessorT2((FloatProcessor
)outside_mask
.convertToFloat());
1807 if (null == alpha
) {
1809 alpha_mask
= outside_mask
;
1817 //Utils.logAll("### k=" + k + " alpha.length=" + (null != alpha ? ((float[])alpha.getPixels()).length : 0) + " image.length=" + ((float[])fp.getPixels()).length);
1819 if (Thread
.currentThread().isInterrupted()) return false;
1821 // 0 - blur the previous image to 0.75 sigma
1822 if (0 != k
) { // not doing so at the end because it would add one unnecessary blurring
1823 fp
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])fp
.getPixels(), sw
, sh
), 0.75f
).data
, cm
);
1824 if (null != alpha
) {
1825 alpha
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])alpha
.getPixels(), sw
, sh
), 0.75f
).data
, null);
1826 if (alpha
!= outside
&& outside
!= null) {
1827 outside
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])outside
.getPixels(), sw
, sh
), 0.75f
).data
, null);
1831 // 1 - check that the target folder for the desired scale exists
1832 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1833 if (null == target_dir
) continue;
1834 // 2 - generate scaled image
1836 fp
= (FloatProcessor
)fp
.resize(w
, h
);
1837 if (ImagePlus
.GRAY8
== type
) {
1838 fp
.setMinAndMax(0, 255); // the min and max was expanded into 0,255 range at convertToFloat for 8-bit images, so the only limit to be added now to the FloatProcessor is that of the 8-bit range. The latter is done automatically for FloatProcessor class, but FloatProcessorT2 doesn't, to avoid the expensive (and here superfluous) operation of looping through all pixels in the findMinAndMax method.
1840 fp
.setMinAndMax(patch
.getMin(), patch
.getMax()); // Must be done: the resize doesn't preserve the min and max!
1842 if (null != alpha
) {
1843 alpha
= (FloatProcessor
)alpha
.resize(w
, h
);
1844 if (alpha
!= outside
&& null != outside
) {
1845 outside
= (FloatProcessor
)outside
.resize(w
, h
);
1849 if (null != alpha
) {
1850 // 3 - save as jpeg with alpha
1851 final byte[] a
= (byte[])alpha
.convertToByte(false).getPixels();
1852 if (null != outside
) {
1854 if (alpha
!= outside
) {
1855 o
= (byte[])outside
.convertToByte(false).getPixels();
1859 // Remove all not completely inside pixels from the alpha mask
1860 // If there was no alpha mask, alpha is the outside itself
1861 for (int i
=0; i
<o
.length
; i
++) {
1862 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;
1865 if (ImagePlus
.GRAY8
!= type
) { // for 8-bit, the min,max has been applied when going to FloatProcessor
1866 fp
.setMinAndMax(patch
.getMin(), patch
.getMax());
1868 final int[] pix
= embedAlpha((int[])fp
.convertToRGB().getPixels(), a
);
1870 final BufferedImage bi_save
= createARGBImage(w
, h
, pix
);
1871 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi_save
, target_dir
+ filename
, 0.85f
)) {
1872 cannot_regenerate
.add(patch
);
1878 // 3 - save as 8-bit jpeg
1879 final ImageProcessor ip2
= Utils
.convertTo(fp
, type
, false); // no scaling, since the conversion to float above didn't change the range. This is needed because of the min and max
1880 if (!coordinate_transformed
) ip2
.setMinAndMax(patch
.getMin(), patch
.getMax()); // Must be done, it's a new ImageProcessor
1881 if (null != cm
) ip2
.setColorModel(cm
); // the LUT
1883 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(ip2
, target_dir
+ filename
, 0.85f
, as_grey
)) {
1884 cannot_regenerate
.add(patch
);
1889 // 4 - prepare values for the next scaled image
1895 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
1898 //final StopWatch timer = new StopWatch();
1900 // use java hardware-accelerated resizing
1901 Image awt
= ip
.createImage();
1903 BufferedImage balpha
= null == alpha_mask ?
null : convertToBufferedImage(alpha_mask
);
1904 BufferedImage boutside
= null == outside_mask ?
null : convertToBufferedImage(outside_mask
);
1906 BufferedImage bi
= null;
1907 final Object hint
= getHint(resizing_mode
);
1912 if (Thread
.currentThread().isInterrupted()) return false;
1914 // check that the target folder for the desired scale exists
1915 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1916 if (null == target_dir
) continue;
1917 // obtain half image
1918 // for level 0 and others, when awt is not a BufferedImage or needs to be reduced in size (to new w,h)
1919 final BufferedImage
[] res
= IToBI(awt
, w
, h
, hint
, balpha
, boutside
);
1923 // prepare next iteration
1924 if (awt
!= bi
) awt
.flush();
1929 // save this iteration
1930 if ( ( (null != balpha
|| null != boutside
) &&
1931 !ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi
, target_dir
+ filename
, 0.85f
))
1932 || ( null == balpha
&& null == boutside
&& !ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(bi
, target_dir
+ filename
, 0.85f
, as_grey
))) {
1933 cannot_regenerate
.add(patch
);
1936 } while (w
>= 32 && h
>= 32);
1939 //timer.cumulative();
1943 // flush any cached tiles
1944 flushMipMaps(patch
.getId());
1947 } catch (Throwable e
) {
1949 cannot_regenerate
.add(patch
);
1952 // gets executed even when returning from the catch statement or within the try/catch block
1953 synchronized (gm_lock
) {
1955 hs_regenerating_mipmaps
.remove(patch
);
1961 /** Remove the file, if it exists, with serialized features for patch.
1962 * Returns true when no such file or on success; false otherwise. */
1963 public boolean removeSerializedFeatures(final Patch patch
) {
1964 final File f
= new File(new StringBuffer(getUNUIdFolder()).append("features.ser/").append(FSLoader
.createIdPath(Long
.toString(patch
.getId()), "features", ".ser")).toString());
1968 } catch (Exception e
) {
1975 /** Remove the file, if it exists, with serialized point matches for patch.
1976 * Returns true when no such file or on success; false otherwise. */
1977 public boolean removeSerializedPointMatches(final Patch patch
) {
1978 final String ser
= new StringBuffer(getUNUIdFolder()).append("pointmatches.ser/").toString();
1979 final File fser
= new File(ser
);
1981 if (!fser
.exists() || !fser
.isDirectory()) return true;
1983 boolean success
= true;
1984 final String sid
= Long
.toString(patch
.getId());
1986 final ArrayList
<String
> removed_paths
= new ArrayList
<String
>();
1988 // 1 - Remove all files with <p1.id>_<p2.id>:
1989 if (sid
.length() < 2) {
1990 // Delete all files starting with sid + '_' and present directly under fser
1991 success
= Utils
.removePrefixedFiles(fser
, sid
+ "_", removed_paths
);
1993 final String sid_
= sid
+ "_"; // minimal 2 length: a number and the underscore
1994 final int len
= sid_
.length();
1995 final StringBuffer dd
= new StringBuffer();
1996 for (int i
=1; i
<=len
; i
++) {
1997 dd
.append(sid_
.charAt(i
-1));
1998 if (0 == i
% 2 && len
!= i
) dd
.append('/');
2000 final String med
= dd
.toString();
2001 final int last_slash
= med
.lastIndexOf('/');
2002 final File med_parent
= new File(ser
+ med
.substring(0, last_slash
+1));
2003 // case of 12/34/_* ---> use prefix: "_"
2004 // case of 12/34/5_/* ---> use prefix: last number plus underscore, aka: med.substring(med.length()-2);
2005 success
= Utils
.removePrefixedFiles(med_parent
,
2006 last_slash
== med
.length() -2 ?
"_" : med
.substring(med
.length() -2),
2010 // 2 - For each removed path, find the complementary: <*>_<p1.id>
2011 for (String path
: removed_paths
) {
2012 if (IJ
.isWindows()) path
= path
.replace('\\', '/');
2013 File f
= new File(path
);
2014 // Check that its a pointmatches file
2015 int idot
= path
.lastIndexOf(".pointmatches.ser");
2017 Utils
.log2("Not a pointmatches.ser file: can't process " + path
);
2022 int ifolder
= path
.indexOf("pointmatches.ser/");
2024 Utils
.log2("Not in pointmatches.ser/ folder:" + path
);
2027 String dir
= path
.substring(0, ifolder
+ 17);
2029 // Cut the beginning and the end
2030 String name
= path
.substring(dir
.length(), idot
);
2031 Utils
.log2("name: " + name
);
2032 // Remove all path separators
2033 name
= name
.replaceAll("/", "");
2035 int iunderscore
= name
.indexOf('_');
2036 if (-1 == iunderscore
) {
2037 Utils
.log2("No underscore: can't process " + path
);
2040 name
= FSLoader
.createIdPath(new StringBuffer().append(name
.substring(iunderscore
+1)).append('_').append(name
.substring(0, iunderscore
)).toString(), "pointmatches", ".ser");
2042 f
= new File(dir
+ name
);
2045 Utils
.log2("Could not delete " + f
.getAbsolutePath());
2048 Utils
.log2("Deleted pointmatches file " + name
);
2049 // Now remove its parent directories within pointmatches.ser/ directory, if they are empty
2050 int islash
= name
.lastIndexOf('/');
2051 String dirname
= name
;
2052 while (islash
> -1) {
2053 dirname
= dirname
.substring(0, islash
);
2054 if (!Utils
.removeFile(new File(dir
+ dirname
))) {
2055 // directory not empty
2058 islash
= dirname
.lastIndexOf('/');
2062 Utils
.log2("File does not exist: " + dir
+ name
);
2069 /** 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.
2071 * @param al : the list of Patch instances to generate mipmaps for.
2072 * @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.)
2074 public Bureaucrat
generateMipMaps(final ArrayList al
, final boolean overwrite
) {
2075 if (null == al
|| 0 == al
.size()) return null;
2076 if (null == dir_mipmaps
) createMipMapsDir(null);
2077 if (isURL(dir_mipmaps
)) {
2078 Utils
.log("Mipmaps folder is an URL, can't save files into it.");
2081 final Worker worker
= new Worker("Generating MipMaps") {
2083 this.setAsBackground(true);
2084 this.startedWorking();
2087 final Worker wo
= this;
2089 Utils
.log2("starting mipmap generation ..");
2091 final int size
= al
.size();
2092 final Patch
[] pa
= new Patch
[size
];
2093 final Thread
[] threads
= MultiThreading
.newThreads();
2095 final AtomicInteger ai
= new AtomicInteger(0);
2097 for (int ithread
= 0; ithread
< threads
.length
; ++ithread
) {
2098 threads
[ithread
] = new Thread(new Runnable() {
2101 for (int k
= ai
.getAndIncrement(); k
< size
; k
= ai
.getAndIncrement()) {
2102 if (wo
.hasQuitted()) {
2105 wo
.setTaskName("Generating MipMaps " + (k
+1) + "/" + size
);
2107 boolean ow
= overwrite
;
2109 // check if all the files exist. If one doesn't, then overwrite all anyway
2110 int w
= (int)pa
[k
].getWidth();
2111 int h
= (int)pa
[k
].getHeight();
2113 final String filename
= new File(getAbsolutePath(pa
[k
])).getName() + "." + pa
[k
].getId() + ".jpg";
2118 if (!new File(dir_mipmaps
+ level
+ "/" + filename
).exists()) {
2122 } while (w
>= 32 && h
>= 32);
2125 if ( ! generateMipMaps(pa
[k
]) ) {
2126 // some error ocurred
2127 Utils
.log2("Could not generate mipmaps for patch " + pa
[k
]);
2129 } catch (Exception e
) {
2137 MultiThreading
.startAndJoin(threads
);
2139 } catch (Exception e
) {
2143 this.finishedWorking();
2146 return Bureaucrat
.createAndStart(worker
, ((Patch
)al
.get(0)).getProject());
2149 private final String
getLevelDir(final String dir_mipmaps
, final int level
) {
2150 // synch, so that multithreaded generateMipMaps won't collide trying to create dirs
2151 synchronized (db_lock
) {
2153 final String path
= new StringBuffer(dir_mipmaps
).append(level
).append('/').toString();
2154 if (isURL(dir_mipmaps
)) {
2158 final File file
= new File(path
);
2159 if (file
.exists() && file
.isDirectory()) {
2168 } catch (Exception e
) {
2176 /** Returns the near-unique folder for the project hosted by this FSLoader. */
2177 public String
getUNUIdFolder() {
2178 return new StringBuffer(getStorageFolder()).append("trakem2.").append(unuid
).append('/').toString();
2181 private String
obtainUNUIdFolder() {
2182 YesNoCancelDialog yn
= ControlWindow
.makeYesNoCancelDialog("Old .xml version!", "The loaded XML file does not contain an UNUId. Select a UNUId folder?\n:Should look like: trakem2.12345678.12345678.12345678");
2183 if (!yn
.yesPressed()) return null;
2184 DirectoryChooser dc
= new DirectoryChooser("Select UNUId folder");
2185 String unuid_dir
= dc
.getDirectory();
2186 if (null != unuid_dir
) {
2187 unuid_dir
= unuid_dir
.replace('\\', '/');
2188 if (!unuid_dir
.startsWith("trakem2.")) {
2189 Utils
.logAll("Invalid UNUId folder. Try again or cancel.");
2190 return obtainUNUIdFolder();
2192 String
[] nums
= unuid_dir
.split("\\.");
2193 if (nums
.length
!= 4) {
2194 Utils
.logAll("Invalid UNUId folder. Try again or cancel.");
2195 return obtainUNUIdFolder();
2197 for (int i
=1; i
<nums
.length
; i
++) {
2199 long num
= Long
.parseLong(nums
[i
]);
2200 } catch (NumberFormatException nfe
) {
2201 Utils
.logAll("Invalid UNUId folder. Try again or cancel.");
2202 return obtainUNUIdFolder();
2205 // ok, aceptamos pulpo
2206 this.unuid
= unuid_dir
.substring(8);
2207 if (unuid_dir
.lastIndexOf('/') != unuid_dir
.length() -1) {
2208 this.unuid
= this.unuid
.substring(0, this.unuid
.length() -1);
2212 this.dir_mipmaps
= unuid_dir
+ "trakem2.mipmaps/";
2218 /** If parent path is null, it's asked for.*/
2219 private boolean createMipMapsDir(String parent_path
) {
2220 if (null == this.unuid
) this.unuid
= createUNUId(parent_path
);
2221 if (null == parent_path
) {
2222 // try to create it in the same directory where the XML file is
2223 if (null != dir_storage
) {
2224 File f
= new File(getUNUIdFolder() + "/trakem2.mipmaps");
2228 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
2229 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
2232 } catch (Exception e
) {}
2233 } else if (f
.isDirectory()) {
2234 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
2235 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
2238 // else can't use it
2240 // else, ask for a new folder
2241 final DirectoryChooser dc
= new DirectoryChooser("Select MipMaps parent directory");
2242 parent_path
= dc
.getDirectory();
2243 if (null == parent_path
) return false;
2244 parent_path
= parent_path
.replace('\\', '/');
2245 if (!parent_path
.endsWith("/")) parent_path
+= "/";
2247 // examine parent path
2248 final File file
= new File(parent_path
);
2249 if (file
.exists()) {
2250 if (file
.isDirectory()) {
2252 this.dir_mipmaps
= parent_path
+ "trakem2." + unuid
+ "/trakem2.mipmaps/";
2254 File f
= new File(this.dir_mipmaps
);
2257 Utils
.log("Could not create trakem2.mipmaps!");
2260 } catch (Exception e
) {
2265 Utils
.showMessage("Selected parent path is not a directory. Please choose another one.");
2266 return createMipMapsDir(null);
2269 Utils
.showMessage("Parent path does not exist. Please select a new one.");
2270 return createMipMapsDir(null);
2275 /** Remove all mipmap images from the cache, and optionally set the dir_mipmaps to null. */
2276 public void flushMipMaps(boolean forget_dir_mipmaps
) {
2277 if (null == dir_mipmaps
) return;
2278 synchronized (db_lock
) {
2280 if (forget_dir_mipmaps
) this.dir_mipmaps
= null;
2281 mawts
.removeAllPyramids(); // does not remove level 0 awts (i.e. the 100% images)
2286 /** Remove from the cache all images of level larger than zero corresponding to the given patch id. */
2287 public void flushMipMaps(final long id
) {
2288 if (null == dir_mipmaps
) return;
2289 synchronized (db_lock
) {
2292 //mawts.removePyramid(id); // does not remove level 0 awts (i.e. the 100% images)
2293 // Need to remove ALL now, since level 0 is also included as a mipmap:
2294 for (final Image img
: mawts
.remove(id
)) {
2295 if (null != img
) img
.flush();
2297 } catch (Exception e
) { e
.printStackTrace(); }
2302 /** Gets data from the Patch and queues a new task to do the file removal in a separate task manager thread. */
2303 public void removeMipMaps(final Patch p
) {
2304 if (null == dir_mipmaps
) return;
2306 final int width
= (int)p
.getWidth();
2307 final int height
= (int)p
.getHeight();
2308 final String path
= getAbsolutePath(p
);
2309 if (null == path
) return; // missing file
2310 final String filename
= new File(path
).getName() + "." + p
.getId() + ".jpg";
2311 // cue the task in a dispatcher:
2312 dispatcher
.exec(new Runnable() { public void run() { // copy-paste as a replacement for (defmacro ... we luv java
2313 removeMipMaps(createIdPath(Long
.toString(p
.getId()), filename
, ".jpg"), width
, height
);
2315 } catch (Exception e
) {
2320 private void removeMipMaps(final String filename
, final int width
, final int height
) {
2323 int k
= 0; // the level
2325 final File f
= new File(dir_mipmaps
+ k
+ "/" + filename
);
2329 Utils
.log2("Could not remove file " + f
.getAbsolutePath());
2331 } catch (Exception e
) {
2338 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
2341 /** Checks whether this Loader is using a directory of image pyramids for each Patch or not. */
2342 public boolean isMipMapsEnabled() {
2343 return null != dir_mipmaps
;
2346 /** Return the closest level to @param level that exists as a file.
2347 * If no valid path is found for the patch, returns ERROR_PATH_NOT_FOUND.
2349 public int getClosestMipMapLevel(final Patch patch
, int level
) {
2350 if (null == dir_mipmaps
) return 0;
2352 final String path
= getAbsolutePath(patch
);
2353 if (null == path
) return ERROR_PATH_NOT_FOUND
;
2354 final String filename
= new File(path
).getName() + ".jpg";
2355 if (isURL(dir_mipmaps
)) {
2356 if (level
<= 0) return 0;
2357 // choose the smallest dimension
2358 // find max level that keeps dim over 32 pixels
2359 final int lev
= getHighestMipMapLevel(Math
.min(patch
.getWidth(), patch
.getHeight()));
2360 if (level
> lev
) return lev
;
2364 final File f
= new File(new StringBuffer(dir_mipmaps
).append(level
).append('/').append(filename
).toString());
2368 // try the next level
2370 } while (level
>= 0);
2372 } catch (Exception e
) {
2378 /** A temporary list of Patch instances for which a pyramid is being generated. */
2379 final private HashSet hs_regenerating_mipmaps
= new HashSet();
2381 /** A lock for the generation of mipmaps. */
2382 final private Object gm_lock
= new Object();
2383 private boolean gm_locked
= false;
2385 protected final void gm_lock() {
2386 //Utils.printCaller(this, 7);
2387 while (gm_locked
) { try { gm_lock
.wait(); } catch (InterruptedException ie
) {} }
2390 protected final void gm_unlock() {
2391 //Utils.printCaller(this, 7);
2394 gm_lock
.notifyAll();
2398 /** Checks if the mipmap file for the Patch and closest upper level to the desired magnification exists. */
2399 public boolean checkMipMapFileExists(final Patch p
, final double magnification
) {
2400 if (null == dir_mipmaps
) return false;
2401 final int level
= getMipMapLevel(magnification
, maxDim(p
));
2402 if (isURL(dir_mipmaps
)) return true; // just assume that it does
2403 if (new File(dir_mipmaps
+ level
+ "/" + new File(getAbsolutePath(p
)).getName() + "." + p
.getId() + ".jpg").exists()) return true;
2407 final Set
<Patch
> cannot_regenerate
= Collections
.synchronizedSet(new HashSet
<Patch
>());
2409 /** Loads the file containing the scaled image corresponding to the given level (or the maximum possible level, if too large) and returns it as an awt.Image, or null if not found. Will also regenerate the mipmaps, i.e. recreate the pre-scaled jpeg images if they are missing. Does not frees memory on its own. */
2410 protected Image
fetchMipMapAWT(final Patch patch
, final int level
) {
2411 if (null == dir_mipmaps
) {
2412 Utils
.log2("null dir_mipmaps");
2416 // TODO should wait if the file is currently being generated
2417 // (it's somewhat handled by a double-try to open the jpeg image)
2419 final int max_level
= getHighestMipMapLevel(patch
);
2421 //Utils.log2("level is: " + max_level);
2423 final String filename
= getInternalFileName(patch
);
2424 if (null == filename
) {
2425 Utils
.log2("null internal filename!");
2429 //final String path = new StringBuffer(dir_mipmaps).append( level > max_level ? max_level : level ).append('/').append(filename).append('.').append(patch.getId()).append(".jpg").toString();
2431 final String path
= new StringBuffer(dir_mipmaps
).append( level
> max_level ? max_level
: level
).append('/').append(createIdPath(Long
.toString(patch
.getId()), filename
, ".jpg")).toString();
2435 if (patch
.hasAlphaChannel()) {
2436 img
= ImageSaver
.openJpegAlpha(path
);
2438 switch (patch
.getType()) {
2439 case ImagePlus
.GRAY16
:
2440 case ImagePlus
.GRAY8
:
2441 case ImagePlus
.GRAY32
:
2442 img
= ImageSaver
.openGreyJpeg(path
);
2445 IJ
.redirectErrorMessages();
2446 ImagePlus imp
= opener
.openImage(path
); // considers URL as well
2447 if (null != imp
) return patch
.createImage(imp
); // considers c_alphas
2448 //img = patch.adjustChannels(Toolkit.getDefaultToolkit().createImage(path)); // doesn't work
2449 //img = patch.adjustChannels(ImageSaver.openColorJpeg(path)); // doesn't work
2450 //Utils.log2("color jpeg path: "+ path);
2451 //Utils.log2("exists ? " + new File(path).exists());
2455 if (null != img
) return img
;
2458 // if we got so far ... try to regenerate the mipmaps
2459 if (!mipmaps_regen
) {
2463 // check that REALLY the file doesn't exist.
2464 if (cannot_regenerate
.contains(patch
)) {
2465 Utils
.log("Cannot regenerate mipmaps for patch " + patch
);
2469 //Utils.log2("getMipMapAwt: imp is " + imp + " for path " + dir_mipmaps + level + "/" + new File(getAbsolutePath(patch)).getName() + "." + patch.getId() + ".jpg");
2471 // Regenerate in the case of not asking for an image under 32x32
2472 double scale
= 1 / Math
.pow(2, level
);
2473 if (level
>= 0 && patch
.getWidth() * scale
>= 32 && patch
.getHeight() * scale
>= 32 && isMipMapsEnabled()) {
2475 regenerateMipMaps(patch
);
2476 return REGENERATING
;
2478 } catch (Exception e
) {
2484 static private AtomicInteger n_regenerating
= new AtomicInteger(0);
2485 static private ExecutorService regenerator
= null;
2486 static public ExecutorService repainter
= null;
2488 /** Queue the regeneration of mipmaps for the Patch; returns immediately, having submitted the job to an executor queue;
2489 * returns true if the task was submitted, false if not. */
2490 public final boolean regenerateMipMaps(final Patch patch
) {
2491 synchronized (gm_lock
) {
2494 if (hs_regenerating_mipmaps
.contains(patch
)) {
2498 hs_regenerating_mipmaps
.add(patch
);
2499 } catch (Exception e
) {
2507 n_regenerating
.incrementAndGet();
2508 Utils
.log2("SUBMITTING to regen " + patch
);
2509 regenerator
.submit(new Runnable() {
2512 Utils
.showStatus("Regenerating mipmaps (" + n_regenerating
.get() + " to go)");
2513 generateMipMaps(patch
, false);
2514 Display
.repaint(patch
.getLayer());
2515 Utils
.showStatus("");
2516 } catch (Exception e
) {
2519 n_regenerating
.decrementAndGet();
2523 } catch (Exception e
) {
2525 ThreadPoolExecutor tpe
= (ThreadPoolExecutor
) regenerator
;
2526 Utils
.log2("active thread count: " + tpe
.getActiveCount() +
2527 "\ncore pool size: " + tpe
.getCorePoolSize() +
2528 "\ncompleted: " + tpe
.getCompletedTaskCount() +
2529 "\nqueued: " + tpe
.getQueue().size() +
2530 "\ntask count: " + tpe
.getTaskCount());
2536 /** 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.
2537 public long estimateImageFileSize(final Patch p
, final int level
) {
2539 // jpeg image to be loaded:
2540 final double scale
= 1 / Math
.pow(2, level
);
2541 return (long)(p
.getWidth() * scale
* p
.getHeight() * scale
* 5 + 1024);
2543 long size
= (long)(p
.getWidth() * p
.getHeight());
2544 int bytes_per_pixel
= 1;
2545 final int type
= p
.getType();
2547 case ImagePlus
.GRAY32
:
2548 bytes_per_pixel
= 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
2550 case ImagePlus
.GRAY16
:
2551 bytes_per_pixel
= 3; // 2 for the ShortProcessor, and 1 for the pixels8
2552 case ImagePlus
.COLOR_RGB
:
2553 bytes_per_pixel
= 4;
2555 case ImagePlus
.GRAY8
:
2556 case ImagePlus
.COLOR_256
:
2557 bytes_per_pixel
= 1;
2558 // check jpeg, which can only encode RGB (taken care of above) and 8-bit and 8-bit color images:
2559 String path
= ht_paths
.get(p
.getId());
2560 if (null != path
&& path
.endsWith(".jpg")) bytes_per_pixel
= 5; //4 for the int[] and 1 for the byte[]
2563 bytes_per_pixel
= 5; // conservative
2567 return size
* bytes_per_pixel
+ 1024;
2570 public String
makeProjectName() {
2571 if (null == project_file_path
|| 0 == project_file_path
.length()) return super.makeProjectName();
2572 final String name
= new File(project_file_path
).getName();
2573 final int i_dot
= name
.lastIndexOf('.');
2574 if (-1 == i_dot
) return name
;
2575 if (0 == i_dot
) return super.makeProjectName();
2576 return name
.substring(0, i_dot
);
2580 /** Returns the path where the imp is saved to: the storage folder plus a name. */
2581 public String
handlePathlessImage(final ImagePlus imp
) {
2582 final FileInfo fi
= imp
.getOriginalFileInfo();
2583 if (null == fi
.fileName
|| fi
.fileName
.equals("")) {
2584 fi
.fileName
= "img_" + System
.currentTimeMillis() + ".tif";
2586 if (!fi
.fileName
.endsWith(".tif")) fi
.fileName
+= ".tif";
2587 fi
.directory
= dir_storage
;
2588 if (imp
.getNSlices() > 1) {
2589 new FileSaver(imp
).saveAsTiffStack(dir_storage
+ fi
.fileName
);
2591 new FileSaver(imp
).saveAsTiff(dir_storage
+ fi
.fileName
);
2593 Utils
.log2("Saved a copy into the storage folder:\n" + dir_storage
+ fi
.fileName
);
2594 return dir_storage
+ fi
.fileName
;
2597 /** Generates layer-wise mipmaps with constant tile width and height. The mipmaps include only images.
2598 * Mipmaps area generated all the way down until the entire canvas fits within one single tile.
2600 public Bureaucrat
generateLayerMipMaps(final Layer
[] la
, final int starting_level
) {
2601 // hard-coded dimensions for layer mipmaps.
2602 final int WIDTH
= 512;
2603 final int HEIGHT
= 512;
2605 // Each tile needs some coding system on where it belongs. For example in its file name, such as <layer_id>_Xi_Yi
2607 // Generate the starting level mipmaps, and then the others from it by gaussian or whatever is indicated in the project image_resizing_mode property.
2611 /** Convert old-style storage folders to new style. */
2612 public boolean fixStorageFolders() {
2614 // 1 - Create folder unuid_folder at storage_folder + unuid
2615 if (null == this.unuid
) {
2616 Utils
.log2("No unuid for project!");
2619 // the trakem2.<unuid> folder that will now contain trakem2.mipmaps, trakem2.masks, etc.
2620 final String unuid_folder
= getUNUIdFolder();
2621 File fdir
= new File(unuid_folder
);
2622 if (!fdir
.exists()) {
2623 if (!fdir
.mkdir()) {
2624 Utils
.log2("Could not create folder " + unuid_folder
);
2628 // 2 - Create trakem2.mipmaps inside unuid folder
2629 final String new_dir_mipmaps
= unuid_folder
+ "trakem2.mipmaps/";
2630 fdir
= new File(new_dir_mipmaps
);
2631 if (!fdir
.mkdir()) {
2632 Utils
.log2("Could not create folder " + new_dir_mipmaps
);
2635 // 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.
2636 final String dir_mipmaps
= getMipMapsFolder();
2637 for (final String name
: new File(dir_mipmaps
).list()) {
2638 String level_dir
= new StringBuffer(dir_mipmaps
).append(name
).append('/').toString();
2639 final File f
= new File(level_dir
);
2640 if (!f
.isDirectory() || f
.isHidden()) continue;
2641 for (final String mm
: f
.list()) {
2642 if (!mm
.endsWith(".jpg")) continue;
2643 // parse the mipmap file: filename + '.' + id + '.jpg'
2644 int last_dot
= mm
.lastIndexOf('.');
2645 if (-1 == last_dot
) continue;
2646 int prev_last_dot
= mm
.lastIndexOf('.', last_dot
-1);
2647 String id
= mm
.substring(prev_last_dot
+1, last_dot
);
2648 String filename
= mm
.substring(0, prev_last_dot
);
2649 File oldf
= new File(level_dir
+ mm
);
2650 File newf
= new File(new StringBuffer(new_dir_mipmaps
).append(name
).append('/').append(createIdPath(id
, filename
, ".jpg")).toString());
2651 File fd
= newf
.getParentFile();
2654 Utils
.log2("Could not create parent dir " + fd
.getAbsolutePath());
2657 if (!oldf
.renameTo(newf
)) {
2658 Utils
.log2("Could not move mipmap file " + oldf
.getAbsolutePath() + " to " + newf
.getAbsolutePath());
2664 this.dir_mipmaps
= new_dir_mipmaps
;
2666 // Remove old empty dirs:
2667 Utils
.removeFile(new File(dir_mipmaps
));
2669 // 4 - same for alpha folder and features folder.
2670 final String masks_folder
= getStorageFolder() + "trakem2.masks/";
2671 File fmasks
= new File(masks_folder
);
2672 this.dir_masks
= null;
2673 if (fmasks
.exists()) {
2674 final String new_dir_masks
= unuid_folder
+ "trakem2.masks/";
2675 for (final File fmask
: fmasks
.listFiles()) {
2676 final String name
= fmask
.getName();
2677 if (!name
.endsWith(".zip")) continue;
2678 int last_dot
= name
.lastIndexOf('.');
2679 if (-1 == last_dot
) continue;
2680 int prev_last_dot
= name
.lastIndexOf('.', last_dot
-1);
2681 String id
= name
.substring(prev_last_dot
+1, last_dot
);
2682 String filename
= name
.substring(0, prev_last_dot
);
2683 File newf
= new File(new_dir_masks
+ createIdPath(id
, filename
, ".zip"));
2684 File fd
= newf
.getParentFile();
2687 Utils
.log2("Could not create parent dir " + fd
.getAbsolutePath());
2690 if (!fmask
.renameTo(newf
)) {
2691 Utils
.log2("Could not move mask file " + fmask
.getAbsolutePath() + " to " + newf
.getAbsolutePath());
2696 this.dir_masks
= new_dir_masks
;
2698 // remove old empty:
2699 Utils
.removeFile(fmasks
);
2702 // TODO should save the .xml file, so the unuid and the new storage folders are set in there!
2705 } catch (Exception e
) {
2711 /** For Patch id=12345 creates 12/34/5.${filename}.jpg */
2712 static public final String
createMipMapRelPath(final Patch p
) {
2713 return createIdPath(Long
.toString(p
.getId()), new File(p
.getCurrentPath()).getName(), ".jpg");
2716 /** For sid=12345 creates 12/34/5.${filename}.jpg
2717 * Will be fine with other filename-valid chars in sid. */
2718 static public final String
createIdPath(final String sid
, final String filename
, final String ext
) {
2719 final StringBuffer sf
= new StringBuffer(((sid
.length() * 3) / 2) + 1);
2720 final int len
= sid
.length();
2721 for (int i
=1; i
<=len
; i
++) {
2722 sf
.append(sid
.charAt(i
-1));
2723 if (0 == i
% 2 && len
!= i
) sf
.append('/');
2725 return sf
.append('.').append(filename
).append(ext
).toString();
2728 public String
getUNUId() {