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
;
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
.Future
;
80 import java
.util
.concurrent
.ExecutorService
;
81 import java
.util
.concurrent
.Executors
;
82 import java
.util
.concurrent
.ThreadPoolExecutor
;
83 import java
.util
.regex
.Pattern
;
86 /** 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. */
87 public final class FSLoader
extends Loader
{
89 /** Largest id seen so far. */
90 private long max_id
= -1;
91 private final HashMap
<Long
,String
> ht_paths
= new HashMap
<Long
,String
>();
92 /** For saving and overwriting. */
93 private String project_file_path
= null;
94 /** Path to the directory hosting the file image pyramids. */
95 private String dir_mipmaps
= null;
96 /** Path to the directory the user provided when creating the project. */
97 private String dir_storage
= null;
98 /** Path to the directory hosting the alpha masks. */
99 private String dir_masks
= null;
101 /** Path to dir_storage + "trakem2.images/" */
102 private String dir_image_storage
= null;
104 /** Queue and execute Runnable tasks. */
105 static private Dispatcher dispatcher
= new Dispatcher();
107 private Set
<Patch
> touched_mipmaps
= Collections
.synchronizedSet(new HashSet
<Patch
>());
109 private Set
<Patch
> mipmaps_to_remove
= Collections
.synchronizedSet(new HashSet
<Patch
>());
111 /** Used to open a project from an existing XML file. */
114 super.v_loaders
.remove(this); //will be readded on successful open
115 FSLoader
.startStaticServices();
118 private String unuid
= null;
120 /** Used to create a new project, NOT from an XML file. */
121 public FSLoader(final String storage_folder
) {
123 if (null == storage_folder
) this.dir_storage
= super.getStorageFolder(); // home dir
124 else this.dir_storage
= storage_folder
;
125 if (!this.dir_storage
.endsWith("/")) this.dir_storage
+= "/";
126 if (!Loader
.canReadAndWriteTo(dir_storage
)) {
127 Utils
.log("WARNING can't read/write to the storage_folder at " + dir_storage
);
129 this.unuid
= createUNUId(this.dir_storage
);
130 createMipMapsDir(this.dir_storage
);
135 private String
createUNUId(String dir_storage
) {
136 synchronized (db_lock
) {
139 if (null == dir_storage
) dir_storage
= System
.getProperty("user.dir") + "/";
140 return new StringBuffer(64).append(System
.currentTimeMillis()).append('.')
141 .append(Math
.abs(dir_storage
.hashCode())).append('.')
142 .append(Math
.abs(System
.getProperty("user.name").hashCode()))
144 } catch (Exception e
) {
153 /** Create a new FSLoader copying some key parameters such as preprocessor plugin, and storage and mipmap folders. Used for creating subprojects. */
154 public FSLoader(final Loader source
) {
156 this.dir_storage
= source
.getStorageFolder(); // can never be null
157 this.dir_mipmaps
= source
.getMipMapsFolder();
158 if (null == this.dir_mipmaps
) createMipMapsDir(this.dir_storage
);
159 setPreprocessor(source
.getPreprocessor());
162 /** 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. */
163 private void crashDetector() {
164 if (null == dir_mipmaps
) {
165 Utils
.log2("Could NOT create crash detection system: null dir_mipmaps.");
168 File f
= new File(dir_mipmaps
+ ".open.t2");
169 Utils
.log2("Crash detector file is " + dir_mipmaps
+ ".open.t2");
173 askAndExecMipmapRegeneration("TrakEM2 detected a crash!");
175 if (!f
.createNewFile() && !dir_mipmaps
.startsWith("http:")) {
176 Utils
.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
178 Utils
.log2("Created crash detection system.");
181 } catch (Exception e
) {
182 Utils
.log2("Crash detector error:" + e
);
187 public String
getProjectXMLPath() {
188 if (null == project_file_path
) return null;
189 return project_file_path
.toString(); // a copy of it
192 /** 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. */
193 public String
getStorageFolder() {
194 if (null == dir_storage
) return super.getStorageFolder(); // the user's home
195 return dir_storage
.toString(); // a copy
198 /** Returns a folder proven to be writable for images can be stored into. */
199 public String
getImageStorageFolder() {
200 if (null == dir_image_storage
) {
201 String s
= getUNUIdFolder() + "trakem2.images/";
202 File f
= new File(s
);
203 if (f
.exists() && f
.isDirectory() && f
.canWrite()) {
204 dir_image_storage
= s
;
205 return dir_image_storage
;
210 dir_image_storage
= s
;
211 } catch (Exception e
) {
213 return getStorageFolder(); // fall back
217 return dir_image_storage
;
220 /** Returns TMLHandler.getProjectData() . If the path is null it'll be asked for. */
221 public Object
[] openFSProject(String path
, final boolean open_displays
) {
222 // clean path of double-slashes, safely (and painfully)
224 path
= path
.replace('\\','/');
226 int itwo
= path
.indexOf("//");
228 if (0 == itwo
/* samba disk */
229 || (5 == itwo
&& "http:".equals(path
.substring(0, 5)))) {
232 path
= path
.substring(0, itwo
) + path
.substring(itwo
+1);
234 itwo
= path
.indexOf("//", itwo
+1);
239 String user
= System
.getProperty("user.name");
240 OpenDialog od
= new OpenDialog("Select Project", OpenDialog
.getDefaultDirectory(), null);
241 String file
= od
.getFileName();
242 if (null == file
|| file
.toLowerCase().startsWith("null")) return null;
243 String dir
= od
.getDirectory().replace('\\', '/');
244 if (!dir
.endsWith("/")) dir
+= "/";
245 this.project_file_path
= dir
+ file
;
246 Utils
.log2("project file path 1: " + this.project_file_path
);
248 this.project_file_path
= path
;
249 Utils
.log2("project file path 2: " + this.project_file_path
);
251 Utils
.log2("Loader.openFSProject: path is " + path
);
252 // check if any of the open projects uses the same file path, and refuse to open if so:
253 if (null != FSLoader
.getOpenProject(project_file_path
, this)) {
254 Utils
.showMessage("The project is already open.");
258 Object
[] data
= null;
260 // parse file, according to expected format as indicated by the extension:
261 if (this.project_file_path
.toLowerCase().endsWith(".xml")) {
262 InputStream i_stream
= null;
263 TMLHandler handler
= new TMLHandler(this.project_file_path
, this);
264 if (handler
.isUnreadable()) {
268 SAXParserFactory factory
= SAXParserFactory
.newInstance();
269 factory
.setValidating(true);
270 SAXParser parser
= factory
.newSAXParser();
271 if (isURL(this.project_file_path
)) {
272 i_stream
= new java
.net
.URL(this.project_file_path
).openStream();
274 i_stream
= new BufferedInputStream(new FileInputStream(this.project_file_path
));
276 InputSource input_source
= new InputSource(i_stream
);
277 setMassiveMode(true);
278 parser
.parse(input_source
, handler
);
279 } catch (java
.io
.FileNotFoundException fnfe
) {
280 Utils
.log("ERROR: File not found: " + path
);
282 } catch (Exception e
) {
286 setMassiveMode(false);
287 if (null != i_stream
) {
290 } catch (Exception e
) {
296 if (null == handler
) {
297 Utils
.showMessage("Error when reading the project .xml file.");
301 data
= handler
.getProjectData(open_displays
);
305 Utils
.showMessage("Error when parsing the project .xml file.");
309 super.v_loaders
.add(this);
314 // Only one thread at a time may access this method.
315 synchronized static private final Project
getOpenProject(final String project_file_path
, final Loader caller
) {
316 if (null == v_loaders
) return null;
317 final Loader
[] lo
= (Loader
[])v_loaders
.toArray(new Loader
[0]); // atomic way to get the list of loaders
318 for (int i
=0; i
<lo
.length
; i
++) {
319 if (lo
[i
].equals(caller
)) continue;
320 if (lo
[i
] instanceof FSLoader
&& ((FSLoader
)lo
[i
]).project_file_path
.equals(project_file_path
)) {
321 return Project
.findProject(lo
[i
]);
327 static public final Project
getOpenProject(final String project_file_path
) {
328 return getOpenProject(project_file_path
, null);
331 public boolean isReady() {
332 return null != ht_paths
;
335 static private void startStaticServices() {
336 if (null == dispatcher
|| dispatcher
.isQuit()) dispatcher
= new Dispatcher();
337 int np
= Runtime
.getRuntime().availableProcessors();
339 // 2 cores = 2 threads
340 // 3+ cores = cores-1 threads
342 if (null == regenerator
|| regenerator
.isShutdown()) {
343 regenerator
= Executors
.newFixedThreadPool(np
);
345 if (null == repainter
|| repainter
.isShutdown()) {
346 repainter
= Executors
.newFixedThreadPool(np
); // for SnapshotPanel
350 /** Shutdown the various thread pools and disactivate services in general. */
351 static private void destroyStaticServices() {
352 if (null != regenerator
) regenerator
.shutdownNow();
353 if (null != dispatcher
) dispatcher
.quit();
354 if (null != repainter
) repainter
.shutdownNow();
357 public void destroy() {
359 Utils
.showStatus("", false);
360 // delete mipmap files that where touched and not cleared as saved (i.e. the project was not saved)
361 touched_mipmaps
.addAll(mipmaps_to_remove
);
362 for (final Patch p
: touched_mipmaps
) {
363 File f
= new File(getAbsolutePath(p
)); // with slice info appended
364 //Utils.log2("File f is " + f);
365 Utils
.log2("Removing mipmaps for " + p
);
366 // Cannot run in the dispatcher: is a daemon, and would be interrupted.
367 removeMipMaps(createIdPath(Long
.toString(p
.getId()), f
.getName(), ".jpg"), (int)p
.getWidth(), (int)p
.getHeight());
370 // remove empty trakem2.mipmaps folder if any
371 if (null != dir_mipmaps
&& !dir_mipmaps
.equals(dir_storage
)) {
372 File f
= new File(dir_mipmaps
);
373 if (f
.isDirectory() && 0 == f
.list(new FilenameFilter() {
374 public boolean accept(File fdir
, String name
) {
375 File file
= new File(dir_mipmaps
+ name
);
376 if (file
.isHidden() || '.' == name
.charAt(0)) return false;
380 try { f
.delete(); } catch (Exception e
) { Utils
.log("Could not remove empty trakem2.mipmaps directory."); }
383 // remove crash detector
385 File fm
= new File(dir_mipmaps
+ ".open.t2");
387 Utils
.log2("WARNING: could not delete crash detector file .open.t2 from trakem2.mipmaps folder at " + dir_mipmaps
);
389 } catch (Exception e
) {
390 Utils
.log2("WARNING: crash detector file trakem.mipmaps/.open.t2 may NOT have been deleted.");
393 if (null == ControlWindow
.getProjects() || 1 == ControlWindow
.getProjects().size()) {
394 destroyStaticServices();
396 // remove unuid dir if xml_path is empty (i.e. never saved and not opened from an .xml file)
397 if (null == project_file_path
) {
398 Utils
.log2("Removing unuid dir, since project was never saved.");
399 final File f
= new File(getUNUIdFolder());
400 if (null != dir_mipmaps
) Utils
.removePrefixedFiles(f
, "trakem2.mipmaps", null);
401 if (null != dir_masks
) Utils
.removePrefixedFiles(f
, "trakem2.masks", null);
402 Utils
.removePrefixedFiles(f
, "features.ser", null);
403 Utils
.removePrefixedFiles(f
, "pointmatches.ser", null);
405 if (f
.isDirectory()) {
408 Utils
.log2("Could not delete unuid directory: likely not empty!");
410 } catch (Exception e
) {
411 Utils
.log2("Could not delete unuid directory: " + e
);
417 /** Get the next unique id, not shared by any other object within the same project. */
418 public long getNextId() {
420 synchronized (db_lock
) {
428 /** Loaded in full from XML file */
429 public double[][][] fetchBezierArrays(long id
) {
433 /** Loaded in full from XML file */
434 public ArrayList
fetchPipePoints(long id
) {
438 /** Loaded in full from XML file */
439 public ArrayList
fetchBallPoints(long id
) {
443 /** Loaded in full from XML file */
444 public Area
fetchArea(long area_list_id
, long layer_id
) {
448 /* 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().
449 * or just use the Patch.getImageProcessor() method which does it for you. */
450 public ImagePlus
fetchImagePlus(final Patch p
) {
451 return (ImagePlus
)fetchImage(p
, Layer
.IMAGEPLUS
);
454 /** Fetch the ImageProcessor in a synchronized manner, so that there are no conflicts in retrieving the ImageProcessor for a specific stack slice, for example.
455 * Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImageProcessor,
456 * or just use the Patch.getImageProcessor() method which does it for you. */
457 public ImageProcessor
fetchImageProcessor(final Patch p
) {
458 return (ImageProcessor
)fetchImage(p
, Layer
.IMAGEPROCESSOR
);
461 /** So far accepts Layer.IMAGEPLUS and Layer.IMAGEPROCESSOR as format. */
462 public Object
fetchImage(final Patch p
, final int format
) {
463 ImagePlus imp
= null;
464 ImageProcessor ip
= null;
468 PatchLoadingLock plock
= null;
469 synchronized (db_lock
) {
471 imp
= imps
.get(p
.getId());
473 path
= getAbsolutePath(p
);
475 if (null != path
) i_sl
= path
.lastIndexOf("-----#slice=");
477 // activate proper slice
479 // check that the stack is large enough (user may have changed it)
480 final int ia
= Integer
.parseInt(path
.substring(i_sl
+ 12));
481 if (ia
<= imp
.getNSlices()) {
482 if (null == imp
.getStack() || null == imp
.getStack().getPixels(ia
)) {
483 // reload (happens when closing a stack that was opened before importing it, and then trying to paint, for example)
484 imps
.remove(p
.getId());
489 case Layer
.IMAGEPROCESSOR
:
490 ip
= imp
.getStack().getProcessor(ia
);
493 case Layer
.IMAGEPLUS
:
497 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
503 return null; // beyond bonds!
507 // for non-stack images
511 case Layer
.IMAGEPROCESSOR
:
512 return imp
.getProcessor();
513 case Layer
.IMAGEPLUS
:
516 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
521 slice
= path
.substring(i_sl
);
523 path
= path
.substring(0, i_sl
);
526 releaseMemory(); // ensure there is a minimum % of free memory
527 plock
= getOrMakePatchLoadingLock(p
, 0);
528 } catch (Exception e
) {
537 synchronized (plock
) {
540 imp
= imps
.get(p
.getId());
542 // was loaded by a different thread -- TODO the slice of the stack could be wrong!
545 case Layer
.IMAGEPROCESSOR
:
546 return imp
.getProcessor();
547 case Layer
.IMAGEPLUS
:
550 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
559 synchronized (db_lock
) {
561 n_bytes
= estimateImageFileSize(p
, 0);
562 max_memory
-= n_bytes
;
566 releaseToFit(n_bytes
);
567 imp
= openImage(path
);
571 synchronized (db_lock
) {
574 max_memory
+= n_bytes
;
577 if (!hs_unloadable
.contains(p
)) {
578 Utils
.log("FSLoader.fetchImagePlus: no image exists for patch " + p
+ " at path " + path
);
579 hs_unloadable
.add(p
);
581 if (ControlWindow
.isGUIEnabled()) {
582 FilePathRepair
.add(p
);
584 removePatchLoadingLock(plock
);
589 // update all clients of the stack, if any
591 String rel_path
= getPath(p
); // possibly relative
592 final int r_isl
= rel_path
.lastIndexOf("-----#slice");
593 if (-1 != r_isl
) rel_path
= rel_path
.substring(0, r_isl
); // should always happen
594 for (Iterator
<Map
.Entry
<Long
,String
>> it
= ht_paths
.entrySet().iterator(); it
.hasNext(); ) {
595 final Map
.Entry
<Long
,String
> entry
= it
.next();
596 final String str
= entry
.getValue(); // this is like calling getPath(p)
597 //Utils.log2("processing " + str);
598 if (0 != str
.indexOf(rel_path
)) {
599 //Utils.log2("SKIP str is: " + str + "\t but path is: " + rel_path);
600 continue; // get only those whose path is identical, of course!
602 final int isl
= str
.lastIndexOf("-----#slice=");
604 //int i_slice = Integer.parseInt(str.substring(isl + 12));
605 final long lid
= entry
.getKey();
609 // set proper active slice
610 final int ia
= Integer
.parseInt(slice
.substring(12));
612 if (Layer
.IMAGEPROCESSOR
== format
) ip
= imp
.getStack().getProcessor(ia
); // otherwise creates one new for nothing
614 // for non-stack images
615 // OBSOLETE and wrong //p.putMinAndMax(imp); // non-destructive contrast: min and max -- WRONG, it's destructive for ColorProcessor and ByteProcessor!
616 // puts the Patch min and max values into the ImagePlus processor.
617 imps
.put(p
.getId(), imp
);
618 if (Layer
.IMAGEPROCESSOR
== format
) ip
= imp
.getProcessor();
620 // imp is cached, so:
621 removePatchLoadingLock(plock
);
623 } catch (Exception e
) {
629 case Layer
.IMAGEPROCESSOR
:
630 return ip
; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
631 case Layer
.IMAGEPLUS
:
634 Utils
.log("FSLoader.fetchImage: Unknown format " + format
);
642 /** Returns the alpha mask image from a file, or null if none stored. */
644 public ByteProcessor
fetchImageMask(final Patch p
) {
645 // Else, see if there is a file for the Patch:
646 final String path
= getAlphaPath(p
);
647 if (null == path
) return null;
648 // Open the mask image, which should be a compressed float tif.
649 final ImagePlus imp
= opener
.openImage(path
);
651 //Utils.log2("No mask found or could not open mask image for patch " + p + " from " + path);
654 final ByteProcessor mask
= (ByteProcessor
)imp
.getProcessor().convertToByte(false);
655 //Utils.log2("Mask dimensions: " + mask.getWidth() + " x " + mask.getHeight() + " for patch " + p);
656 if (mask
.getWidth() != p
.getOWidth() || mask
.getHeight() != p
.getOHeight()) {
657 Utils
.log2("Mask has improper dimensions: " + mask
.getWidth() + " x " + mask
.getHeight() + " for patch " + p
+ " which is of " + p
.getOWidth() + " x " + p
.getOHeight());
664 public String
getAlphaPath(final Patch p
) {
665 final String filename
= getInternalFileName(p
);
666 if (null == filename
) {
667 Utils
.log2("null filepath!");
670 final String dir
= getMasksFolder();
671 return new StringBuffer(dir
).append(createIdPath(Long
.toString(p
.getId()), filename
, ".zip")).toString();
675 public void storeAlphaMask(final Patch p
, final ByteProcessor fp
) {
676 // would fail if user deletes the trakem2.masks/ folder from the storage folder after having set dir_masks. But that is his problem.
677 final String path
= getAlphaPath(p
);
678 File parent
= new File(path
).getParentFile();
680 IJ
.redirectErrorMessages();
681 new FileSaver(new ImagePlus("mask", fp
)).saveAsZip(getAlphaPath(p
));
684 public final String
getMasksFolder() {
685 if (null == dir_masks
) createMasksFolder();
689 synchronized private final void createMasksFolder() {
690 if (null == dir_masks
) dir_masks
= getUNUIdFolder() + "trakem2.masks/";
691 final File f
= new File(dir_masks
);
692 if (f
.exists() && f
.isDirectory()) return;
695 } catch (Exception e
) {
700 /** Remove the file containing the given Patch's alpha mask. */
701 public final boolean removeAlphaMask(final Patch p
) {
703 File f
= new File(getAlphaPath(p
));
708 } catch (Exception e
) {
714 /** Loaded in full from XML file */
715 public Object
[] fetchLabel(DLabel label
) {
719 /** Loads and returns the original image, which is not cached, or returns null if it's not different than the working image. */
720 synchronized public ImagePlus
fetchOriginal(final Patch patch
) {
721 String original_path
= patch
.getOriginalPath();
722 if (null == original_path
) return null;
723 // else, reserve memory and open it:
724 long n_bytes
= estimateImageFileSize(patch
, 0);
726 synchronized (db_lock
) {
728 max_memory
-= n_bytes
;
732 return openImage(original_path
);
733 } catch (Throwable t
) {
736 synchronized (db_lock
) {
738 max_memory
+= n_bytes
;
745 public void prepare(Layer layer
) {
746 //Utils.log2("FSLoader.prepare(Layer): not implemented.");
747 super.prepare(layer
);
750 /* GENERIC, from DBObject calls. Records the id of the object in the HashMap ht_dbo.
751 * Always returns true. Does not check if another object has the same id.
753 public boolean addToDatabase(final DBObject ob
) {
754 synchronized (db_lock
) {
757 final long id
= ob
.getId();
766 public boolean updateInDatabase(final DBObject ob
, final String key
) {
767 // Should only be GUI-driven
770 if (ob
.getClass() == Patch
.class) {
772 if (key
.equals("tiff_working")) return null != setImageFile(p
, fetchImagePlus(p
));
777 public boolean removeFromDatabase(final DBObject ob
) {
778 synchronized (db_lock
) {
781 // remove from the hashtable
782 final long loid
= ob
.getId();
783 Utils
.log2("removing " + Project
.getName(ob
.getClass()) + " " + ob
);
784 if (ob
.getClass() == Patch
.class) {
785 // STRATEGY change: images are not owned by the FSLoader.
787 if (!ob
.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p
);
788 ht_paths
.remove(p
.getId()); // after removeMipMaps !
789 mawts
.removeAndFlush(loid
);
790 final ImagePlus imp
= imps
.remove(loid
);
792 if (imp
.getStackSize() > 1) {
793 if (null == imp
.getProcessor()) {}
794 else if (null == imp
.getProcessor().getPixels()) {}
795 else Loader
.flush(imp
); // only once
800 cannot_regenerate
.remove(p
);
802 flushMipMaps(p
.getId()); // locks on its own
803 touched_mipmaps
.remove(p
);
811 /** 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.
812 * If the Patch p current image path is different than its original image path, then the file is overwritten if it exists already.
814 public String
setImageFile(final Patch p
, final ImagePlus imp
) {
815 if (null == imp
) return null;
817 String path
= getAbsolutePath(p
);
820 // path can be null if the image is pasted, or from a copy, or totally new
822 int i_sl
= path
.lastIndexOf("-----#slice=");
824 slice
= path
.substring(i_sl
);
825 path
= path
.substring(0, i_sl
);
828 // no path, inspect image FileInfo's path if the image has no changes
830 final FileInfo fi
= imp
.getOriginalFileInfo();
831 if (null != fi
&& null != fi
.directory
&& null != fi
.fileName
) {
832 final String fipath
= fi
.directory
.replace('\\', '/') + "/" + fi
.fileName
;
833 if (new File(fipath
).exists()) {
834 // no need to save a new image, it exists and has no changes
835 updatePaths(p
, fipath
, null != slice
);
837 Utils
.log2("Reusing image file: path exists for fileinfo at " + fipath
);
844 final String starting_path
= path
;
845 // Save as a separate image in a new path within the storage folder
847 String filename
= path
.substring(path
.lastIndexOf('/') +1);
849 //Utils.log2("filename 1: " + filename);
851 // remove .tif extension if there
852 if (filename
.endsWith(".tif")) filename
= filename
.substring(0, filename
.length() -3); // keep the dot
854 //Utils.log2("filename 2: " + filename);
856 // check if file ends with a tag of form ".id1234." where 1234 is p.getId()
857 final String tag
= ".id" + p
.getId() + ".";
858 if (!filename
.endsWith(tag
)) filename
+= tag
.substring(1); // without the starting dot, since it has one already
859 // reappend extension
862 //Utils.log2("filename 3: " + filename);
864 path
= getImageStorageFolder() + filename
;
866 if (path
.equals(p
.getOriginalPath())) {
867 // Houston, we have a problem: a user reused a non-original image
870 final int itag
= path
.lastIndexOf(tag
);
872 path
= path
.substring(0, itag
) + "." + i
+ tag
+ "tif";
874 file
= new File(path
);
875 } while (file
.exists());
878 //Utils.log2("path to use: " + path);
880 final String path2
= super.exportImage(p
, imp
, path
, true);
882 //Utils.log2("path exported to: " + path2);
884 // update paths' hashtable
886 updatePaths(p
, path2
, null != slice
);
888 hs_unloadable
.remove(p
);
891 Utils
.log("WARNING could not save image at " + path
);
893 updatePaths(p
, starting_path
, null != slice
);
897 } catch (Exception e
) {
905 * Never used. Was this planned to be what we do no with DBObject.getUniqueId()?
907 private final String
makeFileTitle(final Patch p
) {
908 String title
= p
.getTitle();
909 if (null == title
) return "image-" + p
.getId();
910 title
= asSafePath(title
);
911 if (0 == title
.length()) return "image-" + p
.getId();
915 /** Associate patch with imp, and all slices as well if any. */
916 private void cacheAll(final Patch p
, final ImagePlus imp
) {
918 for (Patch pa
: p
.getStackPatches()) {
926 /** For the Patch and for any associated slices if the patch is part of a stack. */
927 private void updatePaths(final Patch patch
, final String path
, final boolean is_stack
) {
928 synchronized (db_lock
) {
931 // ensure the old path is cached in the Patch, to get set as the original if there is no original.
933 for (Patch p
: patch
.getStackPatches()) {
934 long pid
= p
.getId();
935 String str
= ht_paths
.get(pid
);
936 int isl
= str
.lastIndexOf("-----#slice=");
937 updatePatchPath(p
, path
+ str
.substring(isl
));
940 Utils
.log2("path to set: " + path
);
941 Utils
.log2("path before: " + ht_paths
.get(patch
.getId()));
942 updatePatchPath(patch
, path
);
943 Utils
.log2("path after: " + ht_paths
.get(patch
.getId()));
945 } catch (Throwable e
) {
953 /** With slice info appended at the end; only if it exists, otherwise null. */
954 public String
getAbsolutePath(final Patch patch
) {
955 String abs_path
= patch
.getCurrentPath();
956 if (null != abs_path
) return abs_path
;
957 // else, compute, set and return it:
958 String path
= ht_paths
.get(patch
.getId());
959 if (null == path
) return null;
960 // substract slice info if there
961 int i_sl
= path
.lastIndexOf("-----#slice=");
964 slice
= path
.substring(i_sl
);
965 path
= path
.substring(0, i_sl
);
967 if (isRelativePath(path
)) {
968 // path is relative: preprend the parent folder of the xml file
969 path
= getParentFolder() + path
;
970 if (!isURL(path
) && !new File(path
).exists()) {
971 Utils
.log("Path for patch " + patch
+ " does not exist: " + path
);
974 // else assume that it exists
976 // reappend slice info if existent
977 if (null != slice
) path
+= slice
;
979 patch
.cacheCurrentPath(path
);
983 public final String
getImageFilePath(final Patch p
) {
984 final String path
= getAbsolutePath(p
);
985 if (null == path
) return null;
986 final int i
= path
.lastIndexOf("-----#slice");
987 return -1 == i ? path
988 : path
.substring(0, i
);
991 public static final boolean isURL(final String path
) {
992 return null != path
&& 0 == path
.indexOf("http://");
995 static public final Pattern ABS_PATH
= Pattern
.compile("^[a-zA-Z]*:/.*$|^/.*$|[a-zA-Z]:.*$");
997 public static final boolean isRelativePath(final String path
) {
998 return ! ABS_PATH
.matcher(path
).matches();
1001 /** All backslashes are converted to slashes to avoid havoc in MSWindows. */
1002 public void addedPatchFrom(String path
, final Patch patch
) {
1004 Utils
.log("Null path for patch: " + patch
);
1007 updatePatchPath(patch
, path
);
1010 /** This method has the exclusivity in calling ht_paths.put, because it ensures the path won't have escape characters. */
1011 private final void updatePatchPath(final Patch patch
, String path
) { // reversed order in purpose, relative to addedPatchFrom
1012 // avoid W1nd0ws nightmares
1013 path
= path
.replace('\\', '/'); // replacing with chars, in place
1014 // remove double slashes that a user may have slipped in
1015 final int start
= isURL(path
) ?
6 : (IJ
.isWindows() ?
3 : 1);
1016 while (-1 != path
.indexOf("//", start
)) {
1017 // avoid the potential C:// of windows and the starting // of a samba network
1018 path
= path
.substring(0, start
) + path
.substring(start
).replace("//", "/");
1020 // cache path as absolute
1021 patch
.cacheCurrentPath(isRelativePath(path
) ?
getParentFolder() + path
: path
);
1022 // if path is absolute, try to make it relative
1023 //Utils.log2("path was: " + path);
1024 path
= makeRelativePath(path
);
1026 ht_paths
.put(patch
.getId(), path
);
1027 //Utils.log2("Updated patch path " + ht_paths.get(patch.getId()) + " for patch " + patch);
1030 /** Takes a String and returns a copy with the following conversions: / to -, space to _, and \ to -. */
1031 public String
asSafePath(final String name
) {
1032 return name
.trim().replace('/', '-').replace(' ', '_').replace('\\','-');
1035 /** 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. */
1036 public String
save(final Project project
) {
1037 String result
= null;
1038 if (null == project_file_path
) {
1039 String xml_path
= super.saveAs(project
, null, false);
1040 if (null == xml_path
) return null;
1042 this.project_file_path
= xml_path
;
1043 ControlWindow
.updateTitle(project
);
1044 result
= this.project_file_path
;
1047 File fxml
= new File(project_file_path
);
1048 result
= super.export(project
, fxml
, false);
1050 if (null != result
) {
1051 Utils
.logAll(Utils
.now() + " Saved " + project
);
1052 touched_mipmaps
.clear();
1057 public String
saveAs(Project project
) {
1058 String path
= super.saveAs(project
, null, false);
1060 // update the xml path to point to the new one
1061 this.project_file_path
= path
;
1062 Utils
.log2("After saveAs, new xml path is: " + path
);
1064 ControlWindow
.updateTitle(project
);
1068 /** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
1069 public String
saveAs(final String path
, final boolean overwrite
) {
1071 Utils
.log("Cannot save on null path.");
1074 String path2
= path
;
1075 if (!path2
.endsWith(".xml")) path2
+= ".xml";
1076 File fxml
= new File(path2
);
1077 if (!fxml
.canWrite()) {
1078 // write to storage folder instead
1079 String path3
= path2
;
1080 path2
= getStorageFolder() + fxml
.getName();
1081 Utils
.logAll("WARNING can't write to " + path3
+ "\n --> will write instead to " + path2
);
1082 fxml
= new File(path2
);
1086 while (fxml
.exists()) {
1087 String parent
= fxml
.getParent().replace('\\','/');
1088 if (!parent
.endsWith("/")) parent
+= "/";
1089 String name
= fxml
.getName();
1090 name
= name
.substring(0, name
.length() - 4);
1091 path2
= parent
+ name
+ "-" + i
+ ".xml";
1092 fxml
= new File(path2
);
1096 Project project
= Project
.findProject(this);
1097 path2
= super.saveAs(project
, path2
, false);
1098 if (null != path2
) {
1099 project_file_path
= path2
;
1100 Utils
.logAll("After saveAs, new xml path is: " + path2
);
1101 ControlWindow
.updateTitle(project
);
1102 touched_mipmaps
.clear();
1107 /** Returns the stored path for the given Patch image, which may be relative and may contain slice information appended.*/
1108 public String
getPath(final Patch patch
) {
1109 return ht_paths
.get(patch
.getId());
1112 /** 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. */
1113 private String
makeRelativePath(String path
) {
1114 if (null == project_file_path
) {
1121 // fix W1nd0ws paths
1122 path
= path
.replace('\\', '/'); // char-based, no parsing problems
1124 String slice
= null;
1125 int isl
= path
.lastIndexOf("-----#slice");
1127 slice
= path
.substring(isl
);
1128 path
= path
.substring(0, isl
);
1131 if (isRelativePath(path
)) {
1133 if (-1 != isl
) path
+= slice
;
1136 // the long and verbose way, to be cross-platform. Should work with URLs just the same.
1137 String xdir
= new File(project_file_path
).getParentFile().getAbsolutePath();
1138 if (!xdir
.endsWith("/")) xdir
+= "/";
1139 if (IJ
.isWindows()) {
1140 xdir
= xdir
.replace('\\', '/');
1141 path
= path
.replace('\\', '/');
1143 if (path
.startsWith(xdir
)) {
1144 path
= path
.substring(xdir
.length());
1146 if (-1 != isl
) path
+= slice
;
1147 //Utils.log("made relative path: " + path);
1151 /** Adds a "Save" and "Save as" menu items. */
1152 public void setupMenuItems(final JMenu menu
, final Project project
) {
1153 ActionListener listener
= new ActionListener() {
1154 public void actionPerformed(ActionEvent ae
) {
1155 String command
= ae
.getActionCommand();
1156 if (command
.equals("Save")) {
1158 } else if (command
.equals("Save as...")) {
1164 item
= new JMenuItem("Save"); item
.addActionListener(listener
); menu
.add(item
);
1165 item
.setAccelerator(KeyStroke
.getKeyStroke(KeyEvent
.VK_S
, 0, true));
1166 item
= new JMenuItem("Save as..."); item
.addActionListener(listener
); menu
.add(item
);
1169 /** Returns the last Patch. */
1170 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
) {
1171 Utils
.log2("FSLoader.importStackAsPatches filepath=" + filepath
);
1172 String target_dir
= null;
1174 DirectoryChooser dc
= new DirectoryChooser("Folder to save images");
1175 target_dir
= dc
.getDirectory();
1176 if (null == target_dir
) return null; // user canceled dialog
1177 if (target_dir
.length() -1 != target_dir
.lastIndexOf('/')) {
1182 final boolean virtual
= imp_stack
.getStack().isVirtual();
1184 int pos_x
= Integer
.MAX_VALUE
!= x ? x
: (int)first_layer
.getLayerWidth()/2 - imp_stack
.getWidth()/2;
1185 int pos_y
= Integer
.MAX_VALUE
!= y ? y
: (int)first_layer
.getLayerHeight()/2 - imp_stack
.getHeight()/2;
1186 final double thickness
= first_layer
.getThickness();
1187 final String title
= Utils
.removeExtension(imp_stack
.getTitle()).replace(' ', '_');
1188 Utils
.showProgress(0);
1189 Patch previous_patch
= null;
1190 final int n
= imp_stack
.getStackSize();
1191 for (int i
=1; i
<=n
; i
++) {
1192 Layer layer
= first_layer
;
1193 double z
= first_layer
.getZ() + (i
-1) * thickness
;
1194 if (i
> 1) layer
= first_layer
.getParent().getLayer(z
, thickness
, true); // will create new layer if not found
1195 if (null == layer
) {
1196 Utils
.log("Display.importStack: could not create new layers.");
1199 String patch_path
= null;
1201 ImagePlus imp_patch_i
= null;
1202 if (virtual
) { // because we love inefficiency, every time all this is done again
1203 VirtualStack vs
= (VirtualStack
)imp_stack
.getStack();
1204 String vs_dir
= vs
.getDirectory().replace('\\', '/');
1205 if (!vs_dir
.endsWith("/")) vs_dir
+= "/";
1206 String iname
= vs
.getFileName(i
);
1207 patch_path
= vs_dir
+ iname
;
1208 Utils
.log2("virtual stack: patch path is " + patch_path
);
1210 Utils
.log2(i
+ " : " + patch_path
);
1211 imp_patch_i
= openImage(patch_path
);
1213 ImageProcessor ip
= imp_stack
.getStack().getProcessor(i
);
1214 if (as_copy
) ip
= ip
.duplicate();
1215 imp_patch_i
= new ImagePlus(title
+ "__slice=" + i
, ip
);
1217 preProcess(imp_patch_i
);
1219 String label
= imp_stack
.getStack().getSliceLabel(i
);
1220 if (null == label
) label
= "";
1223 patch_path
= target_dir
+ imp_patch_i
.getTitle() + ".zip";
1224 ini
.trakem2
.io
.ImageSaver
.saveAsZip(imp_patch_i
, patch_path
);
1225 patch
= new Patch(project
, label
+ " " + title
+ " " + i
, pos_x
, pos_y
, imp_patch_i
);
1226 } else if (virtual
) {
1227 patch
= new Patch(project
, label
, pos_x
, pos_y
, imp_patch_i
);
1229 patch_path
= filepath
+ "-----#slice=" + i
;
1230 //Utils.log2("path is "+ patch_path);
1231 final AffineTransform atp
= new AffineTransform();
1232 atp
.translate(pos_x
, pos_y
);
1233 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
);
1234 patch
.addToDatabase();
1235 //Utils.log2("type is " + imp_stack.getType());
1237 Utils
.log2("B: " + i
+ " : " + patch_path
);
1238 addedPatchFrom(patch_path
, patch
);
1239 if (!as_copy
&& !virtual
) {
1240 if (virtual
) cache(patch
, imp_patch_i
); // each slice separately
1241 else cache(patch
, imp_stack
); // uses the entire stack, shared among all Patch instances
1243 if (isMipMapsEnabled()) generateMipMaps(patch
);
1244 if (null != previous_patch
) patch
.link(previous_patch
);
1246 previous_patch
= patch
;
1247 Utils
.showProgress(i
* (1.0 / n
));
1249 Utils
.showProgress(1.0);
1251 // update calibration
1254 // return the last patch
1255 return previous_patch
;
1258 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1259 public void parseXMLOptions(final HashMap ht_attributes
) {
1260 Object ob
= ht_attributes
.remove("preprocessor");
1262 setPreprocessor((String
)ob
);
1264 // Adding some logic to support old projects which lack a storage folder and a mipmaps folder
1265 // and also to prevent errors such as those created when manualy tinkering with the XML file
1266 // or renaming directories, etc.
1267 ob
= ht_attributes
.remove("storage_folder");
1269 String sf
= ((String
)ob
).replace('\\', '/');
1270 if (isRelativePath(sf
)) {
1271 sf
= getParentFolder() + sf
;
1275 Utils
.log2("Can't have an URL as the path of a storage folder.");
1277 File f
= new File(sf
);
1278 if (f
.exists() && f
.isDirectory()) {
1279 this.dir_storage
= sf
;
1281 Utils
.log2("storage_folder was not found or is invalid: " + ob
);
1285 if (null == this.dir_storage
) {
1286 // select the directory where the xml file lives.
1287 this.dir_storage
= getParentFolder();
1288 if (null == this.dir_storage
|| isURL(this.dir_storage
)) this.dir_storage
= null;
1289 if (null == this.dir_storage
&& ControlWindow
.isGUIEnabled()) {
1290 Utils
.log2("Asking user for a storage folder in a dialog."); // tip for headless runners whose program gets "stuck"
1291 DirectoryChooser dc
= new DirectoryChooser("REQUIRED: select a storage folder");
1292 this.dir_storage
= dc
.getDirectory();
1294 if (null == this.dir_storage
) {
1295 IJ
.showMessage("TrakEM2 requires a storage folder.\nTemporarily your home directory will be used.");
1296 this.dir_storage
= System
.getProperty("user.home").replace('\\', '/');
1300 if (null != this.dir_storage
&& !this.dir_storage
.endsWith("/")) this.dir_storage
+= "/";
1301 Utils
.log2("storage folder is " + this.dir_storage
);
1303 ob
= ht_attributes
.remove("mipmaps_folder");
1305 String mf
= ((String
)ob
).replace('\\', '/');
1306 if (isRelativePath(mf
)) {
1307 mf
= getParentFolder() + mf
;
1310 this.dir_mipmaps
= mf
;
1311 // TODO must disable input somehow, so that images are not edited.
1313 File f
= new File(mf
);
1314 if (f
.exists() && f
.isDirectory()) {
1315 this.dir_mipmaps
= mf
;
1317 Utils
.log2("mipmaps_folder was not found or is invalid: " + ob
);
1322 // parse the unuid before attempting to create any folders
1323 this.unuid
= (String
) ht_attributes
.remove("unuid");
1325 // Attempt to get an existing UNUId folder, for .xml files that share the same mipmaps folder
1326 if (ControlWindow
.isGUIEnabled() && null == this.unuid
) {
1327 obtainUNUIdFolder();
1330 if (null == this.dir_mipmaps
) {
1331 // create a new one inside the dir_storage, which can't be null
1332 createMipMapsDir(dir_storage
);
1333 if (null != this.dir_mipmaps
&& ControlWindow
.isGUIEnabled() && null != IJ
.getInstance()) {
1334 askAndExecMipmapRegeneration(null);
1338 if (null != this.dir_mipmaps
&& !this.dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
1339 Utils
.log2("mipmaps folder is " + this.dir_mipmaps
);
1341 if (null == unuid
) {
1342 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.");
1343 Utils
.log2("Creating unuid for project " + this);
1344 this.unuid
= createUNUId(dir_storage
);
1345 fixStorageFolders();
1346 Utils
.log2("Now mipmaps folder is " + this.dir_mipmaps
);
1347 if (null != dir_masks
) Utils
.log2("Now masks folder is " + this.dir_masks
);
1351 private void askAndExecMipmapRegeneration(final String msg
) {
1352 Utils
.log2("Asking user Yes/No to generate mipmaps on the background."); // tip for headless runners whose program gets "stuck"
1353 YesNoDialog yn
= new YesNoDialog(IJ
.getInstance(), "Generate mipmaps", (null != msg ? msg
+ "\n" : "") + "Generate mipmaps in the background for all images?\nWhen in doubt say 'no', and do it later if necessary from popup 'Project' submenu.");
1354 if (yn
.yesPressed()) {
1355 final Loader lo
= this;
1359 // wait while parsing the rest of the XML file
1360 while (!v_loaders
.contains(lo
)) {
1363 Project pj
= Project
.findProject(lo
);
1364 // Submit a task for each Patch:
1365 for (final Displayable patch
: pj
.getRootLayerSet().getDisplayables(Patch
.class)) {
1366 ((FSLoader
)lo
).regenerateMipMaps((Patch
)patch
);
1368 } catch (Exception e
) {}
1374 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1375 public void insertXMLOptions(StringBuffer sb_body
, String indent
) {
1376 sb_body
.append(indent
).append("unuid=\"").append(unuid
).append("\"\n");
1377 if (null != preprocessor
) sb_body
.append(indent
).append("preprocessor=\"").append(preprocessor
).append("\"\n");
1378 if (null != dir_mipmaps
) sb_body
.append(indent
).append("mipmaps_folder=\"").append(makeRelativePath(dir_mipmaps
)).append("\"\n");
1379 if (null != dir_storage
) sb_body
.append(indent
).append("storage_folder=\"").append(makeRelativePath(dir_storage
)).append("\"\n");
1382 /** Return the path to the folder containing the project XML file. */
1383 private final String
getParentFolder() {
1384 return this.project_file_path
.substring(0, this.project_file_path
.lastIndexOf('/')+1);
1387 /* ************** MIPMAPS **********************/
1389 /** Returns the path to the directory hosting the file image pyramids. */
1390 public String
getMipMapsFolder() {
1396 static private IndexColorModel thresh_cm = null;
1398 static private final IndexColorModel getThresholdLUT() {
1399 if (null == thresh_cm) {
1400 // An array of all black pixels (value 0) except at 255, which is white (value 255).
1401 final byte[] c = new byte[256];
1403 thresh_cm = new IndexColorModel(8, 256, c, c, c);
1409 /** Returns the array of pixels, whose type depends on the bi.getType(); for example, for a BufferedImage.TYPE_BYTE_INDEXED, returns a byte[]. */
1410 static public final Object
grabPixels(final BufferedImage bi
) {
1411 final PixelGrabber pg
= new PixelGrabber(bi
, 0, 0, bi
.getWidth(), bi
.getHeight(), false);
1414 return pg
.getPixels();
1415 } catch (InterruptedException e
) {
1421 private final BufferedImage
createCroppedAlpha(final BufferedImage alpha
, final BufferedImage outside
) {
1422 if (null == outside
) return alpha
;
1424 final int width
= outside
.getWidth();
1425 final int height
= outside
.getHeight();
1427 // Create an outside image, thresholded: only pixels of 255 remain as 255, the rest is set to 0.
1428 /* // DOESN'T work: creates a mask with "black" as 254 (???), and white 255 (correct).
1429 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, getThresholdLUT());
1430 thresholded.createGraphics().drawImage(outside, 0, 0, null);
1433 // So, instead: grab the pixels, fix them manually
1434 // The cast to byte[] works because "outside" and "alpha" are TYPE_BYTE_INDEXED.
1435 final byte[] o
= (byte[])grabPixels(outside
);
1436 if (null == o
) return null;
1437 final byte[] a
= null == alpha ? o
: (byte[])grabPixels(alpha
);
1439 // Set each non-255 pixel in outside to 0 in alpha:
1440 for (int i
=0; i
<o
.length
; i
++) {
1441 if ( (o
[i
]&0xff) < 255) a
[i
] = 0;
1444 // Put the pixels back into an image:
1445 final BufferedImage thresholded
= new BufferedImage(width
, height
, BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1446 thresholded
.getRaster().setDataElements(0, 0, width
, height
, a
);
1451 static public final BufferedImage
convertToBufferedImage(final ByteProcessor bp
) {
1452 bp
.setMinAndMax(0, 255);
1453 final Image img
= bp
.createImage();
1454 if (img
instanceof BufferedImage
) return (BufferedImage
)img
;
1456 final BufferedImage bi
= new BufferedImage(bp
.getWidth(), bp
.getHeight(), BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1457 bi
.createGraphics().drawImage(bi
, 0, 0, null);
1461 /** Scale a BufferedImage.TYPE_BYTE_INDEXED into another of the same type but dimensions target_width,target_height. */
1462 static private final BufferedImage
scaleAndFlush(final Image img
, final int target_width
, final int target_height
, final boolean area_averaging
, final Object interpolation_hint
) {
1463 final BufferedImage bi
= new BufferedImage(target_width
, target_height
, BufferedImage
.TYPE_BYTE_INDEXED
, Loader
.GRAY_LUT
);
1464 if (area_averaging
) {
1465 bi
.createGraphics().drawImage(img
.getScaledInstance(target_width
, target_height
, Image
.SCALE_AREA_AVERAGING
), 0, 0, null);
1467 final Graphics2D g
= bi
.createGraphics();
1468 g
.setRenderingHint(RenderingHints
.KEY_INTERPOLATION
, interpolation_hint
);
1469 g
.drawImage(img
, 0, 0, target_width
, target_height
, null); // draws it scaled to target area w*h
1471 // Release native resources
1477 /** 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. */
1478 private final BufferedImage
[] IToBI(final Image awt
, final int w
, final int h
, final Object interpolation_hint
, final BufferedImage alpha
, final BufferedImage outside
) {
1480 final boolean area_averaging
= interpolation_hint
.getClass() == Integer
.class && Loader
.AREA_AVERAGING
== ((Integer
)interpolation_hint
).intValue();
1481 final boolean must_scale
= (w
!= awt
.getWidth(null) || h
!= awt
.getHeight(null));
1483 if (null != alpha
|| null != outside
) bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_INT_ARGB
);
1484 else bi
= new BufferedImage(w
, h
, BufferedImage
.TYPE_BYTE_GRAY
);
1485 final Graphics2D g
= bi
.createGraphics();
1486 if (area_averaging
) {
1487 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.
1488 g
.drawImage(img
, 0, 0, null);
1490 g
.setRenderingHint(RenderingHints
.KEY_INTERPOLATION
, interpolation_hint
);
1491 g
.drawImage(awt
, 0, 0, w
, h
, null); // draws it scaled
1493 BufferedImage ba
= alpha
;
1494 BufferedImage bo
= outside
;
1495 if (null != alpha
&& must_scale
) {
1496 ba
= scaleAndFlush(alpha
, w
, h
, area_averaging
, interpolation_hint
);
1498 if (null != outside
&& must_scale
) {
1499 bo
= scaleAndFlush(outside
, w
, h
, area_averaging
, interpolation_hint
);
1502 BufferedImage the_alpha
= ba
;
1503 if (null != alpha
) {
1504 if (null != outside
) {
1505 the_alpha
= createCroppedAlpha(ba
, bo
);
1507 } else if (null != outside
) {
1508 the_alpha
= createCroppedAlpha(null, bo
);
1510 if (null != the_alpha
) {
1511 bi
.getAlphaRaster().setRect(the_alpha
.getRaster());
1512 //bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])new ImagePlus("", the_alpha).getProcessor().convertToFloat().getPixels());
1516 //Utils.log2("bi is: " + bi.getType() + " BufferedImage.TYPE_INT_ARGB=" + BufferedImage.TYPE_INT_ARGB);
1519 FloatProcessor fp_alpha = null;
1520 fp_alpha = (FloatProcessor) new ByteProcessor(ba).convertToFloat();
1521 // Set all non-white pixels to zero (eliminate shadowy border caused by interpolation)
1522 final float[] pix = (float[])fp_alpha.getPixels();
1523 for (int i=0; i<pix.length; i++)
1524 if (Math.abs(pix[i] - 255) > 0.001f) pix[i] = 0;
1525 bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])fp_alpha.getPixels());
1528 return new BufferedImage
[]{bi
, ba
, bo
};
1531 private final Object
getHint(final int mode
) {
1533 case Loader
.BICUBIC
:
1534 return RenderingHints
.VALUE_INTERPOLATION_BICUBIC
;
1535 case Loader
.BILINEAR
:
1536 return RenderingHints
.VALUE_INTERPOLATION_BILINEAR
;
1537 case Loader
.AREA_AVERAGING
:
1538 return new Integer(mode
);
1539 case Loader
.NEAREST_NEIGHBOR
:
1541 return RenderingHints
.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
;
1545 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1546 static final private byte[] gaussianBlurResizeInHalf(final FloatProcessorT2 source
, final int source_width
, final int source_height
, final int target_width
, final int target_height
) {
1547 source
.setPixels(source_width
, source_height
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])source
.getPixels(), source_width
, source_height
), 0.75f
).data
);
1548 source
.resizeInPlace(target_width
, target_height
);
1549 return (byte[])source
.convertToByte(false).getPixels(); // no scaling
1552 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1553 static final private byte[] meanResizeInHalf(final FloatProcessorT2 source
, final int sourceWidth
, final int sourceHeight
, final int targetWidth
, final int targetHeight
) {
1554 final float[] sourceData
= source
.getFloatPixels();
1555 final float[] targetData
= new float[targetWidth
* targetHeight
];
1557 for (int r
= 0; r
< targetData
.length
; r
+= targetWidth
) {
1559 for (int x
= 0; x
< targetWidth
; ++x
)
1560 targetData
[r
+ x
] = sourceData
[rs
+ ++xs
] + sourceData
[rs
+ ++xs
];
1563 for (int x
= 0; x
< targetWidth
; ++x
) {
1564 targetData
[r
+ x
] += sourceData
[rs
+ ++xs
] + sourceData
[rs
+ ++xs
];
1565 targetData
[r
+ x
] /= 4;
1569 source
.setPixels(targetWidth
, targetHeight
, targetData
);
1570 return (byte[])source
.convertToByte(false).getPixels();
1573 /** Queue/unqueue for mipmap removal on shutdown without saving. */
1574 public void queueForMipmapRemoval(final Patch p
, boolean yes
) {
1575 if (yes
) touched_mipmaps
.add(p
);
1576 else touched_mipmaps
.remove(p
);
1579 /** Queue/unqueue for mipmap removal on shutdown without saving. */
1580 public void tagForMipmapRemoval(final Patch p
, final boolean yes
) {
1581 if (yes
) mipmaps_to_remove
.add(p
);
1582 else mipmaps_to_remove
.remove(p
);
1585 /** Given an image and its source file name (without directory prepended), generate
1586 * a pyramid of images until reaching an image not smaller than 32x32 pixels.<br />
1587 * Such images are stored as jpeg 85% quality in a folder named trakem2.mipmaps.<br />
1588 * The Patch id and a ".jpg" extension will be appended to the filename in all cases.<br />
1589 * Any equally named files will be overwritten.
1591 public boolean generateMipMaps(final Patch patch
) {
1592 return generateMipMaps(patch
, true);
1594 /** 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. */
1595 private boolean generateMipMaps(final Patch patch
, final boolean check_if_already_being_done
) {
1596 Utils
.log2("mipmaps for " + patch
);
1597 final String path
= getAbsolutePath(patch
);
1599 Utils
.log2("generateMipMaps: cannot find path for Patch " + patch
);
1600 cannot_regenerate
.add(patch
);
1603 if (hs_unloadable
.contains(patch
)) {
1604 FilePathRepair
.add(patch
);
1607 synchronized (gm_lock
) {
1610 if (null == dir_mipmaps
) createMipMapsDir(null);
1611 if (null == dir_mipmaps
|| isURL(dir_mipmaps
)) return false;
1612 if (check_if_already_being_done
&& hs_regenerating_mipmaps
.contains(patch
)) {
1613 // already being done
1614 Utils
.log2("Already being done: " + patch
);
1617 hs_regenerating_mipmaps
.add(patch
);
1618 } catch (Exception e
) {
1625 /** Record Patch as modified */
1626 touched_mipmaps
.add(patch
);
1628 /** Remove serialized features, if any */
1629 removeSerializedFeatures(patch
);
1631 /** Remove serialized pointmatches, if any */
1632 removeSerializedPointMatches(patch
);
1634 String srmode
= patch
.getProject().getProperty("image_resizing_mode");
1635 int resizing_mode
= GAUSSIAN
;
1636 if (null != srmode
) resizing_mode
= Loader
.getMode(srmode
);
1640 // 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'.
1641 // 2 - Then (1) should return both the transformed image and the alpha mask
1644 ByteProcessor alpha_mask
= null;
1645 ByteProcessor outside_mask
= null;
1646 final boolean coordinate_transformed
;
1647 int type
= patch
.getType();
1649 // Obtain an image which may be coordinate-transformed, and an alpha mask.
1650 Patch
.PatchImage pai
= patch
.createTransformedImage();
1652 Utils
.log("Can't regenerate mipmaps for patch " + patch
);
1653 cannot_regenerate
.add(patch
);
1657 alpha_mask
= pai
.mask
; // can be null
1658 outside_mask
= pai
.outside
; // can be null
1659 coordinate_transformed
= pai
.coordinate_transformed
;
1663 //final String filename = new StringBuffer(new File(path).getName()).append('.').append(patch.getId()).append(".jpg").toString();
1665 final String filename
= createMipMapRelPath(patch
);
1667 int w
= ip
.getWidth();
1668 int h
= ip
.getHeight();
1670 // sigma = sqrt(2^level - 0.5^2)
1671 // where 0.5 is the estimated sigma for a full-scale image
1672 // which means sigma = 0.75 for the full-scale image (has level 0)
1673 // prepare a 0.75 sigma image from the original
1674 ColorModel cm
= ip
.getColorModel();
1675 int k
= 0; // the scale level. Proper scale is: 1 / pow(2, k)
1676 // but since we scale 50% relative the previous, it's always 0.75
1678 double min
= patch
.getMin(),
1679 max
= patch
.getMax();
1680 // Fix improper min,max values
1681 // (The -1,-1 are flags really for "not set")
1682 if (-1 == min
&& -1 == max
) {
1684 case ImagePlus
.COLOR_RGB
:
1685 case ImagePlus
.COLOR_256
:
1686 case ImagePlus
.GRAY8
:
1687 patch
.setMinAndMax(0, 255);
1689 // Find and flow through to default:
1690 case ImagePlus
.GRAY16
:
1691 ((ij
.process
.ShortProcessor
)ip
).findMinAndMax();
1692 patch
.setMinAndMax(ip
.getMin(), ip
.getMax());
1694 case ImagePlus
.GRAY32
:
1695 ((FloatProcessor
)ip
).findMinAndMax();
1696 patch
.setMinAndMax(ip
.getMin(), ip
.getMax());
1699 min
= patch
.getMin(); // may have changed
1700 max
= patch
.getMax();
1703 // Set for the level 0 image, which is a duplicate of the one on the cache in any case
1704 ip
.setMinAndMax(min
, max
);
1707 // Proper support for LUT images: treat them as RGB
1708 if (ip
.isColorLut()) {
1709 ip
= ip
.convertToRGB();
1711 type
= ImagePlus
.COLOR_RGB
;
1714 if (ImagePlus
.COLOR_RGB
== type
) {
1715 // TODO releaseToFit proper
1716 releaseToFit(w
* h
* 4 * 5);
1717 final ColorProcessor cp
= (ColorProcessor
)ip
;
1718 final FloatProcessorT2 red
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(0, red
);
1719 final FloatProcessorT2 green
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(1, green
);
1720 final FloatProcessorT2 blue
= new FloatProcessorT2(w
, h
, 0, 255); cp
.toFloat(2, blue
);
1721 FloatProcessorT2 alpha
;
1722 final FloatProcessorT2 outside
;
1723 if (null != alpha_mask
) {
1724 alpha
= new FloatProcessorT2((FloatProcessor
)alpha_mask
.convertToFloat());
1728 if (null != outside_mask
) {
1729 outside
= new FloatProcessorT2((FloatProcessor
)outside_mask
.convertToFloat());
1730 if ( null == alpha
) {
1732 alpha_mask
= outside_mask
;
1738 // sw,sh are the dimensions of the image to blur
1739 // w,h are the dimensions to scale the blurred image to
1743 final String target_dir0
= getLevelDir(dir_mipmaps
, 0);
1744 // No alpha channel:
1745 // - use gaussian resizing
1746 // - use standard ImageJ java.awt.Image creation
1748 if (Thread
.currentThread().isInterrupted()) return false;
1750 // Generate level 0 first:
1751 // TODO Add alpha information into the int[] pixel array or make the image visible some ohter way
1752 if (!(null == alpha ? ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(cp
, target_dir0
+ filename
, 0.85f
, false)
1753 : 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
))) {
1754 cannot_regenerate
.add(patch
);
1757 if (Thread
.currentThread().isInterrupted()) return false;
1758 // 1 - Prepare values for the next scaled image
1764 // 2 - Check that the target folder for the desired scale exists
1765 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1766 if (null == target_dir
) continue;
1767 // 3 - Blur the previous image to 0.75 sigma, and scale it
1768 final byte[] r
= gaussianBlurResizeInHalf(red
, sw
, sh
, w
, h
); // will resize 'red' FloatProcessor in place.
1769 final byte[] g
= gaussianBlurResizeInHalf(green
, sw
, sh
, w
, h
); // idem
1770 final byte[] b
= gaussianBlurResizeInHalf(blue
, sw
, sh
, w
, h
); // idem
1771 final byte[] a
= null == alpha ?
null : gaussianBlurResizeInHalf(alpha
, sw
, sh
, w
, h
); // idem
1772 if ( null != outside
) {
1774 if (alpha
!= outside
)
1775 o
= gaussianBlurResizeInHalf(outside
, sw
, sh
, w
, h
); // idem
1778 // Remove all not completely inside pixels from the alphamask
1779 // If there was no alpha mask, alpha is the outside itself
1780 for (int i
=0; i
<o
.length
; i
++) {
1781 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;
1785 // 4 - Compose ColorProcessor
1786 final int[] pix
= new int[w
* h
];
1787 if (null == alpha
) {
1788 for (int i
=0; i
<pix
.length
; i
++) {
1789 pix
[i
] = 0xff000000 | ((r
[i
]&0xff)<<16) | ((g
[i
]&0xff)<<8) | (b
[i
]&0xff);
1791 final ColorProcessor cp2
= new ColorProcessor(w
, h
, pix
);
1793 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(cp2
, target_dir
+ filename
, 0.85f
, false)) {
1794 cannot_regenerate
.add(patch
);
1798 // LIKELY no need to set alpha raster later in createARGBImage ... TODO
1799 for (int i
=0; i
<pix
.length
; i
++) {
1800 pix
[i
] = ((a
[i
]&0xff)<<24) | ((r
[i
]&0xff)<<16) | ((g
[i
]&0xff)<<8) | (b
[i
]&0xff);
1802 final BufferedImage bi_save
= createARGBImage(w
, h
, pix
);
1803 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi_save
, target_dir
+ filename
, 0.85f
)) {
1804 cannot_regenerate
.add(patch
);
1810 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
1814 releaseToFit(w
* h
* 4 * 5);
1815 final boolean as_grey
= !ip
.isColorLut();
1816 if (as_grey
&& null == cm
) {
1820 if (Thread
.currentThread().isInterrupted()) return false;
1822 if (Loader
.GAUSSIAN
== resizing_mode
) {
1823 FloatProcessor fp
= (FloatProcessor
) ip
.convertToFloat();
1826 FloatProcessor alpha
;
1827 FloatProcessor outside
;
1828 if (null != alpha_mask
) {
1829 alpha
= new FloatProcessorT2((FloatProcessor
)alpha_mask
.convertToFloat());
1833 if (null != outside_mask
) {
1834 outside
= new FloatProcessorT2((FloatProcessor
)outside_mask
.convertToFloat());
1835 if (null == alpha
) {
1837 alpha_mask
= outside_mask
;
1845 //Utils.logAll("### k=" + k + " alpha.length=" + (null != alpha ? ((float[])alpha.getPixels()).length : 0) + " image.length=" + ((float[])fp.getPixels()).length);
1847 if (Thread
.currentThread().isInterrupted()) return false;
1849 // 0 - blur the previous image to 0.75 sigma
1850 if (0 != k
) { // not doing so at the end because it would add one unnecessary blurring
1851 fp
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])fp
.getPixels(), sw
, sh
), 0.75f
).data
, cm
);
1852 if (null != alpha
) {
1853 alpha
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])alpha
.getPixels(), sw
, sh
), 0.75f
).data
, null);
1854 if (alpha
!= outside
&& outside
!= null) {
1855 outside
= new FloatProcessorT2(sw
, sh
, ImageFilter
.computeGaussianFastMirror(new FloatArray2D((float[])outside
.getPixels(), sw
, sh
), 0.75f
).data
, null);
1859 // 1 - check that the target folder for the desired scale exists
1860 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1861 if (null == target_dir
) continue;
1862 // 2 - generate scaled image
1864 fp
= (FloatProcessor
)fp
.resize(w
, h
);
1865 if (ImagePlus
.GRAY8
== type
) {
1866 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.
1868 fp
.setMinAndMax(patch
.getMin(), patch
.getMax()); // Must be done: the resize doesn't preserve the min and max!
1870 if (null != alpha
) {
1871 alpha
= (FloatProcessor
)alpha
.resize(w
, h
);
1872 if (alpha
!= outside
&& null != outside
) {
1873 outside
= (FloatProcessor
)outside
.resize(w
, h
);
1877 if (null != alpha
) {
1878 // 3 - save as jpeg with alpha
1879 final byte[] a
= (byte[])alpha
.convertToByte(false).getPixels();
1880 if (null != outside
) {
1882 if (alpha
!= outside
) {
1883 o
= (byte[])outside
.convertToByte(false).getPixels();
1887 // Remove all not completely inside pixels from the alpha mask
1888 // If there was no alpha mask, alpha is the outside itself
1889 for (int i
=0; i
<o
.length
; i
++) {
1890 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;
1893 if (ImagePlus
.GRAY8
!= type
) { // for 8-bit, the min,max has been applied when going to FloatProcessor
1894 fp
.setMinAndMax(patch
.getMin(), patch
.getMax());
1896 final int[] pix
= embedAlpha((int[])fp
.convertToRGB().getPixels(), a
);
1898 final BufferedImage bi_save
= createARGBImage(w
, h
, pix
);
1899 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi_save
, target_dir
+ filename
, 0.85f
)) {
1900 cannot_regenerate
.add(patch
);
1906 // 3 - save as 8-bit jpeg
1907 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
1908 if (!coordinate_transformed
) ip2
.setMinAndMax(patch
.getMin(), patch
.getMax()); // Must be done, it's a new ImageProcessor
1909 if (null != cm
) ip2
.setColorModel(cm
); // the LUT
1911 if (!ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(ip2
, target_dir
+ filename
, 0.85f
, as_grey
)) {
1912 cannot_regenerate
.add(patch
);
1917 // 4 - prepare values for the next scaled image
1923 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
1926 //final StopWatch timer = new StopWatch();
1928 // use java hardware-accelerated resizing
1929 Image awt
= ip
.createImage();
1931 BufferedImage balpha
= null == alpha_mask ?
null : convertToBufferedImage(alpha_mask
);
1932 BufferedImage boutside
= null == outside_mask ?
null : convertToBufferedImage(outside_mask
);
1934 BufferedImage bi
= null;
1935 final Object hint
= getHint(resizing_mode
);
1940 if (Thread
.currentThread().isInterrupted()) return false;
1942 // check that the target folder for the desired scale exists
1943 final String target_dir
= getLevelDir(dir_mipmaps
, k
);
1944 if (null == target_dir
) continue;
1945 // obtain half image
1946 // for level 0 and others, when awt is not a BufferedImage or needs to be reduced in size (to new w,h)
1947 final BufferedImage
[] res
= IToBI(awt
, w
, h
, hint
, balpha
, boutside
);
1951 // prepare next iteration
1952 if (awt
!= bi
) awt
.flush();
1957 // save this iteration
1958 if ( ( (null != balpha
|| null != boutside
) &&
1959 !ini
.trakem2
.io
.ImageSaver
.saveAsJpegAlpha(bi
, target_dir
+ filename
, 0.85f
))
1960 || ( null == balpha
&& null == boutside
&& !ini
.trakem2
.io
.ImageSaver
.saveAsJpeg(bi
, target_dir
+ filename
, 0.85f
, as_grey
))) {
1961 cannot_regenerate
.add(patch
);
1964 } while (w
>= 32 && h
>= 32);
1967 //timer.cumulative();
1971 // flush any cached tiles
1972 flushMipMaps(patch
.getId());
1975 } catch (Throwable e
) {
1977 cannot_regenerate
.add(patch
);
1980 // gets executed even when returning from the catch statement or within the try/catch block
1981 synchronized (gm_lock
) {
1983 hs_regenerating_mipmaps
.remove(patch
);
1989 /** Remove the file, if it exists, with serialized features for patch.
1990 * Returns true when no such file or on success; false otherwise. */
1991 public boolean removeSerializedFeatures(final Patch patch
) {
1992 final File f
= new File(new StringBuffer(getUNUIdFolder()).append("features.ser/").append(FSLoader
.createIdPath(Long
.toString(patch
.getId()), "features", ".ser")).toString());
1996 } catch (Exception e
) {
2003 /** Remove the file, if it exists, with serialized point matches for patch.
2004 * Returns true when no such file or on success; false otherwise. */
2005 public boolean removeSerializedPointMatches(final Patch patch
) {
2006 final String ser
= new StringBuffer(getUNUIdFolder()).append("pointmatches.ser/").toString();
2007 final File fser
= new File(ser
);
2009 if (!fser
.exists() || !fser
.isDirectory()) return true;
2011 boolean success
= true;
2012 final String sid
= Long
.toString(patch
.getId());
2014 final ArrayList
<String
> removed_paths
= new ArrayList
<String
>();
2016 // 1 - Remove all files with <p1.id>_<p2.id>:
2017 if (sid
.length() < 2) {
2018 // Delete all files starting with sid + '_' and present directly under fser
2019 success
= Utils
.removePrefixedFiles(fser
, sid
+ "_", removed_paths
);
2021 final String sid_
= sid
+ "_"; // minimal 2 length: a number and the underscore
2022 final int len
= sid_
.length();
2023 final StringBuffer dd
= new StringBuffer();
2024 for (int i
=1; i
<=len
; i
++) {
2025 dd
.append(sid_
.charAt(i
-1));
2026 if (0 == i
% 2 && len
!= i
) dd
.append('/');
2028 final String med
= dd
.toString();
2029 final int last_slash
= med
.lastIndexOf('/');
2030 final File med_parent
= new File(ser
+ med
.substring(0, last_slash
+1));
2031 // case of 12/34/_* ---> use prefix: "_"
2032 // case of 12/34/5_/* ---> use prefix: last number plus underscore, aka: med.substring(med.length()-2);
2033 success
= Utils
.removePrefixedFiles(med_parent
,
2034 last_slash
== med
.length() -2 ?
"_" : med
.substring(med
.length() -2),
2038 // 2 - For each removed path, find the complementary: <*>_<p1.id>
2039 for (String path
: removed_paths
) {
2040 if (IJ
.isWindows()) path
= path
.replace('\\', '/');
2041 File f
= new File(path
);
2042 // Check that its a pointmatches file
2043 int idot
= path
.lastIndexOf(".pointmatches.ser");
2045 Utils
.log2("Not a pointmatches.ser file: can't process " + path
);
2050 int ifolder
= path
.indexOf("pointmatches.ser/");
2052 Utils
.log2("Not in pointmatches.ser/ folder:" + path
);
2055 String dir
= path
.substring(0, ifolder
+ 17);
2057 // Cut the beginning and the end
2058 String name
= path
.substring(dir
.length(), idot
);
2059 Utils
.log2("name: " + name
);
2060 // Remove all path separators
2061 name
= name
.replaceAll("/", "");
2063 int iunderscore
= name
.indexOf('_');
2064 if (-1 == iunderscore
) {
2065 Utils
.log2("No underscore: can't process " + path
);
2068 name
= FSLoader
.createIdPath(new StringBuffer().append(name
.substring(iunderscore
+1)).append('_').append(name
.substring(0, iunderscore
)).toString(), "pointmatches", ".ser");
2070 f
= new File(dir
+ name
);
2073 Utils
.log2("Could not delete " + f
.getAbsolutePath());
2076 Utils
.log2("Deleted pointmatches file " + name
);
2077 // Now remove its parent directories within pointmatches.ser/ directory, if they are empty
2078 int islash
= name
.lastIndexOf('/');
2079 String dirname
= name
;
2080 while (islash
> -1) {
2081 dirname
= dirname
.substring(0, islash
);
2082 if (!Utils
.removeFile(new File(dir
+ dirname
))) {
2083 // directory not empty
2086 islash
= dirname
.lastIndexOf('/');
2090 Utils
.log2("File does not exist: " + dir
+ name
);
2097 /** 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.
2099 * @param al : the list of Patch instances to generate mipmaps for.
2100 * @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.)
2102 public Bureaucrat
generateMipMaps(final ArrayList al
, final boolean overwrite
) {
2103 if (null == al
|| 0 == al
.size()) return null;
2104 if (null == dir_mipmaps
) createMipMapsDir(null);
2105 if (isURL(dir_mipmaps
)) {
2106 Utils
.log("Mipmaps folder is an URL, can't save files into it.");
2109 final Worker worker
= new Worker("Generating MipMaps") {
2111 this.setAsBackground(true);
2112 this.startedWorking();
2115 final Worker wo
= this;
2117 Utils
.log2("starting mipmap generation ..");
2119 final int size
= al
.size();
2120 final Patch
[] pa
= new Patch
[size
];
2121 final Thread
[] threads
= MultiThreading
.newThreads();
2123 final AtomicInteger ai
= new AtomicInteger(0);
2125 for (int ithread
= 0; ithread
< threads
.length
; ++ithread
) {
2126 threads
[ithread
] = new Thread(new Runnable() {
2129 for (int k
= ai
.getAndIncrement(); k
< size
; k
= ai
.getAndIncrement()) {
2130 if (wo
.hasQuitted()) {
2133 wo
.setTaskName("Generating MipMaps " + (k
+1) + "/" + size
);
2135 boolean ow
= overwrite
;
2137 // check if all the files exist. If one doesn't, then overwrite all anyway
2138 int w
= (int)pa
[k
].getWidth();
2139 int h
= (int)pa
[k
].getHeight();
2141 final String filename
= new File(getAbsolutePath(pa
[k
])).getName() + "." + pa
[k
].getId() + ".jpg";
2146 if (!new File(dir_mipmaps
+ level
+ "/" + filename
).exists()) {
2150 } while (w
>= 32 && h
>= 32);
2153 if ( ! generateMipMaps(pa
[k
]) ) {
2154 // some error ocurred
2155 Utils
.log2("Could not generate mipmaps for patch " + pa
[k
]);
2157 } catch (Exception e
) {
2165 MultiThreading
.startAndJoin(threads
);
2167 } catch (Exception e
) {
2171 this.finishedWorking();
2174 return Bureaucrat
.createAndStart(worker
, ((Patch
)al
.get(0)).getProject());
2177 private final String
getLevelDir(final String dir_mipmaps
, final int level
) {
2178 // synch, so that multithreaded generateMipMaps won't collide trying to create dirs
2179 synchronized (db_lock
) {
2181 final String path
= new StringBuffer(dir_mipmaps
).append(level
).append('/').toString();
2182 if (isURL(dir_mipmaps
)) {
2186 final File file
= new File(path
);
2187 if (file
.exists() && file
.isDirectory()) {
2196 } catch (Exception e
) {
2204 /** Returns the near-unique folder for the project hosted by this FSLoader. */
2205 public String
getUNUIdFolder() {
2206 return new StringBuffer(getStorageFolder()).append("trakem2.").append(unuid
).append('/').toString();
2209 /** Return the unuid_dir or null if none valid selected. */
2210 private String
obtainUNUIdFolder() {
2211 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");
2212 if (!yn
.yesPressed()) return null;
2213 DirectoryChooser dc
= new DirectoryChooser("Select UNUId folder");
2214 String unuid_dir
= dc
.getDirectory();
2215 String unuid_dir_name
= new File(unuid_dir
).getName();
2216 Utils
.log2("Selected UNUId folder: " + unuid_dir
+ "\n with name: " + unuid_dir_name
);
2217 if (null != unuid_dir
) {
2218 unuid_dir
= unuid_dir
.replace('\\', '/');
2219 if ( ! unuid_dir_name
.startsWith("trakem2.")) {
2220 Utils
.logAll("Invalid UNUId folder: must start with \"trakem2.\". Try again or cancel.");
2221 return obtainUNUIdFolder();
2223 String
[] nums
= unuid_dir_name
.split("\\.");
2224 if (nums
.length
!= 4) {
2225 Utils
.logAll("Invalid UNUId folder: needs trakem + 3 number blocks. Try again or cancel.");
2226 return obtainUNUIdFolder();
2228 for (int i
=1; i
<nums
.length
; i
++) {
2230 long num
= Long
.parseLong(nums
[i
]);
2231 } catch (NumberFormatException nfe
) {
2232 Utils
.logAll("Invalid UNUId folder: at least one block is not a number. Try again or cancel.");
2233 return obtainUNUIdFolder();
2236 // ok, aceptamos pulpo
2237 String unuid
= unuid_dir_name
.substring(8); // remove prefix "trakem2."
2238 if (unuid
.endsWith("/")) unuid
= unuid
.substring(0, unuid
.length() -1);
2241 if (!unuid_dir
.endsWith("/")) unuid_dir
+= "/";
2243 String dir_storage
= new File(unuid_dir
).getParent().replace('\\', '/');
2244 if (!dir_storage
.endsWith("/")) dir_storage
+= "/";
2245 this.dir_storage
= dir_storage
;
2247 this.dir_mipmaps
= unuid_dir
+ "trakem2.mipmaps/";
2255 /** If parent path is null, it's asked for.*/
2256 private boolean createMipMapsDir(String parent_path
) {
2257 if (null == this.unuid
) this.unuid
= createUNUId(parent_path
);
2258 if (null == parent_path
) {
2259 // try to create it in the same directory where the XML file is
2260 if (null != dir_storage
) {
2261 File f
= new File(getUNUIdFolder() + "/trakem2.mipmaps");
2265 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
2266 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
2269 } catch (Exception e
) {}
2270 } else if (f
.isDirectory()) {
2271 this.dir_mipmaps
= f
.getAbsolutePath().replace('\\', '/');
2272 if (!dir_mipmaps
.endsWith("/")) this.dir_mipmaps
+= "/";
2275 // else can't use it
2277 // else, ask for a new folder
2278 final DirectoryChooser dc
= new DirectoryChooser("Select MipMaps parent directory");
2279 parent_path
= dc
.getDirectory();
2280 if (null == parent_path
) return false;
2281 parent_path
= parent_path
.replace('\\', '/');
2282 if (!parent_path
.endsWith("/")) parent_path
+= "/";
2284 // examine parent path
2285 final File file
= new File(parent_path
);
2286 if (file
.exists()) {
2287 if (file
.isDirectory()) {
2289 this.dir_mipmaps
= parent_path
+ "trakem2." + unuid
+ "/trakem2.mipmaps/";
2291 File f
= new File(this.dir_mipmaps
);
2294 Utils
.log("Could not create trakem2.mipmaps!");
2297 } catch (Exception e
) {
2302 Utils
.showMessage("Selected parent path is not a directory. Please choose another one.");
2303 return createMipMapsDir(null);
2306 Utils
.showMessage("Parent path does not exist. Please select a new one.");
2307 return createMipMapsDir(null);
2312 /** Remove all mipmap images from the cache, and optionally set the dir_mipmaps to null. */
2313 public void flushMipMaps(boolean forget_dir_mipmaps
) {
2314 if (null == dir_mipmaps
) return;
2315 synchronized (db_lock
) {
2317 if (forget_dir_mipmaps
) this.dir_mipmaps
= null;
2318 mawts
.removeAllPyramids(); // does not remove level 0 awts (i.e. the 100% images)
2323 /** Remove from the cache all images of level larger than zero corresponding to the given patch id. */
2324 public void flushMipMaps(final long id
) {
2325 if (null == dir_mipmaps
) return;
2326 synchronized (db_lock
) {
2329 //mawts.removePyramid(id); // does not remove level 0 awts (i.e. the 100% images)
2330 // Need to remove ALL now, since level 0 is also included as a mipmap:
2331 for (final Image img
: mawts
.remove(id
)) {
2332 if (null != img
) img
.flush();
2334 } catch (Exception e
) { e
.printStackTrace(); }
2339 /** Gets data from the Patch and queues a new task to do the file removal in a separate task manager thread. */
2340 public void removeMipMaps(final Patch p
) {
2341 if (null == dir_mipmaps
) return;
2343 final int width
= (int)p
.getWidth();
2344 final int height
= (int)p
.getHeight();
2345 final String path
= getAbsolutePath(p
);
2346 if (null == path
) return; // missing file
2347 final String filename
= new File(path
).getName() + "." + p
.getId() + ".jpg";
2348 // cue the task in a dispatcher:
2349 dispatcher
.exec(new Runnable() { public void run() { // copy-paste as a replacement for (defmacro ... we luv java
2350 removeMipMaps(createIdPath(Long
.toString(p
.getId()), filename
, ".jpg"), width
, height
);
2352 } catch (Exception e
) {
2357 private void removeMipMaps(final String filename
, final int width
, final int height
) {
2360 int k
= 0; // the level
2362 final File f
= new File(dir_mipmaps
+ k
+ "/" + filename
);
2366 Utils
.log2("Could not remove file " + f
.getAbsolutePath());
2368 } catch (Exception e
) {
2375 } while (w
>= 32 && h
>= 32); // not smaller than 32x32
2378 /** Checks whether this Loader is using a directory of image pyramids for each Patch or not. */
2379 public boolean isMipMapsEnabled() {
2380 return null != dir_mipmaps
;
2383 /** Return the closest level to @param level that exists as a file.
2384 * If no valid path is found for the patch, returns ERROR_PATH_NOT_FOUND.
2386 public int getClosestMipMapLevel(final Patch patch
, int level
) {
2387 if (null == dir_mipmaps
) return 0;
2389 final String path
= getAbsolutePath(patch
);
2390 if (null == path
) return ERROR_PATH_NOT_FOUND
;
2391 final String filename
= new File(path
).getName() + ".jpg";
2392 if (isURL(dir_mipmaps
)) {
2393 if (level
<= 0) return 0;
2394 // choose the smallest dimension
2395 // find max level that keeps dim over 32 pixels
2396 final int lev
= getHighestMipMapLevel(Math
.min(patch
.getWidth(), patch
.getHeight()));
2397 if (level
> lev
) return lev
;
2401 final File f
= new File(new StringBuffer(dir_mipmaps
).append(level
).append('/').append(filename
).toString());
2405 // try the next level
2407 } while (level
>= 0);
2409 } catch (Exception e
) {
2415 /** A temporary list of Patch instances for which a pyramid is being generated. */
2416 final private HashSet hs_regenerating_mipmaps
= new HashSet();
2418 /** A lock for the generation of mipmaps. */
2419 final private Object gm_lock
= new Object();
2420 private boolean gm_locked
= false;
2422 protected final void gm_lock() {
2423 //Utils.printCaller(this, 7);
2424 while (gm_locked
) { try { gm_lock
.wait(); } catch (InterruptedException ie
) {} }
2427 protected final void gm_unlock() {
2428 //Utils.printCaller(this, 7);
2431 gm_lock
.notifyAll();
2435 /** Checks if the mipmap file for the Patch and closest upper level to the desired magnification exists. */
2436 public boolean checkMipMapFileExists(final Patch p
, final double magnification
) {
2437 if (null == dir_mipmaps
) return false;
2438 final int level
= getMipMapLevel(magnification
, maxDim(p
));
2439 if (isURL(dir_mipmaps
)) return true; // just assume that it does
2440 if (new File(dir_mipmaps
+ level
+ "/" + new File(getAbsolutePath(p
)).getName() + "." + p
.getId() + ".jpg").exists()) return true;
2444 final Set
<Patch
> cannot_regenerate
= Collections
.synchronizedSet(new HashSet
<Patch
>());
2446 /** 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. */
2447 protected Image
fetchMipMapAWT(final Patch patch
, final int level
) {
2448 if (null == dir_mipmaps
) {
2449 Utils
.log2("null dir_mipmaps");
2453 // TODO should wait if the file is currently being generated
2454 // (it's somewhat handled by a double-try to open the jpeg image)
2456 final int max_level
= getHighestMipMapLevel(patch
);
2458 //Utils.log2("level is: " + max_level);
2460 final String filename
= getInternalFileName(patch
);
2461 if (null == filename
) {
2462 Utils
.log2("null internal filename!");
2466 //final String path = new StringBuffer(dir_mipmaps).append( level > max_level ? max_level : level ).append('/').append(filename).append('.').append(patch.getId()).append(".jpg").toString();
2468 final String path
= new StringBuffer(dir_mipmaps
).append( level
> max_level ? max_level
: level
).append('/').append(createIdPath(Long
.toString(patch
.getId()), filename
, ".jpg")).toString();
2472 if (patch
.hasAlphaChannel()) {
2473 img
= ImageSaver
.openJpegAlpha(path
);
2475 switch (patch
.getType()) {
2476 case ImagePlus
.GRAY16
:
2477 case ImagePlus
.GRAY8
:
2478 case ImagePlus
.GRAY32
:
2479 img
= ImageSaver
.openGreyJpeg(path
);
2482 IJ
.redirectErrorMessages();
2483 ImagePlus imp
= opener
.openImage(path
); // considers URL as well
2484 if (null != imp
) return patch
.createImage(imp
); // considers c_alphas
2485 //img = patch.adjustChannels(Toolkit.getDefaultToolkit().createImage(path)); // doesn't work
2486 //img = patch.adjustChannels(ImageSaver.openColorJpeg(path)); // doesn't work
2487 //Utils.log2("color jpeg path: "+ path);
2488 //Utils.log2("exists ? " + new File(path).exists());
2492 if (null != img
) return img
;
2495 // if we got so far ... try to regenerate the mipmaps
2496 if (!mipmaps_regen
) {
2500 // check that REALLY the file doesn't exist.
2501 if (cannot_regenerate
.contains(patch
)) {
2502 Utils
.log("Cannot regenerate mipmaps for patch " + patch
);
2506 //Utils.log2("getMipMapAwt: imp is " + imp + " for path " + dir_mipmaps + level + "/" + new File(getAbsolutePath(patch)).getName() + "." + patch.getId() + ".jpg");
2508 // Regenerate in the case of not asking for an image under 32x32
2509 double scale
= 1 / Math
.pow(2, level
);
2510 if (level
>= 0 && patch
.getWidth() * scale
>= 32 && patch
.getHeight() * scale
>= 32 && isMipMapsEnabled()) {
2512 regenerateMipMaps(patch
);
2513 return REGENERATING
;
2515 } catch (Exception e
) {
2521 static private AtomicInteger n_regenerating
= new AtomicInteger(0);
2522 static private ExecutorService regenerator
= null;
2523 static public ExecutorService repainter
= null;
2525 /** Queue the regeneration of mipmaps for the Patch; returns immediately, having submitted the job to an executor queue;
2526 * returns a Future if the task was submitted, null if not. */
2527 public final Future
regenerateMipMaps(final Patch patch
) {
2528 synchronized (gm_lock
) {
2531 if (hs_regenerating_mipmaps
.contains(patch
)) {
2535 hs_regenerating_mipmaps
.add(patch
);
2536 } catch (Exception e
) {
2544 n_regenerating
.incrementAndGet();
2545 Utils
.log2("SUBMITTING to regen " + patch
);
2546 return regenerator
.submit(new Runnable() {
2549 Utils
.showStatus("Regenerating mipmaps (" + n_regenerating
.get() + " to go)");
2550 generateMipMaps(patch
, false);
2551 Display
.repaint(patch
.getLayer());
2552 Utils
.showStatus("");
2553 } catch (Exception e
) {
2556 n_regenerating
.decrementAndGet();
2559 } catch (Exception e
) {
2561 ThreadPoolExecutor tpe
= (ThreadPoolExecutor
) regenerator
;
2562 Utils
.log2("active thread count: " + tpe
.getActiveCount() +
2563 "\ncore pool size: " + tpe
.getCorePoolSize() +
2564 "\ncompleted: " + tpe
.getCompletedTaskCount() +
2565 "\nqueued: " + tpe
.getQueue().size() +
2566 "\ntask count: " + tpe
.getTaskCount());
2572 /** 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.
2573 public long estimateImageFileSize(final Patch p
, final int level
) {
2575 // jpeg image to be loaded:
2576 final double scale
= 1 / Math
.pow(2, level
);
2577 return (long)(p
.getWidth() * scale
* p
.getHeight() * scale
* 5 + 1024);
2579 long size
= (long)(p
.getWidth() * p
.getHeight());
2580 int bytes_per_pixel
= 1;
2581 final int type
= p
.getType();
2583 case ImagePlus
.GRAY32
:
2584 bytes_per_pixel
= 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
2586 case ImagePlus
.GRAY16
:
2587 bytes_per_pixel
= 3; // 2 for the ShortProcessor, and 1 for the pixels8
2588 case ImagePlus
.COLOR_RGB
:
2589 bytes_per_pixel
= 4;
2591 case ImagePlus
.GRAY8
:
2592 case ImagePlus
.COLOR_256
:
2593 bytes_per_pixel
= 1;
2594 // check jpeg, which can only encode RGB (taken care of above) and 8-bit and 8-bit color images:
2595 String path
= ht_paths
.get(p
.getId());
2596 if (null != path
&& path
.endsWith(".jpg")) bytes_per_pixel
= 5; //4 for the int[] and 1 for the byte[]
2599 bytes_per_pixel
= 5; // conservative
2603 return size
* bytes_per_pixel
+ 1024;
2606 public String
makeProjectName() {
2607 if (null == project_file_path
|| 0 == project_file_path
.length()) return super.makeProjectName();
2608 final String name
= new File(project_file_path
).getName();
2609 final int i_dot
= name
.lastIndexOf('.');
2610 if (-1 == i_dot
) return name
;
2611 if (0 == i_dot
) return super.makeProjectName();
2612 return name
.substring(0, i_dot
);
2616 /** Returns the path where the imp is saved to: the storage folder plus a name. */
2617 public String
handlePathlessImage(final ImagePlus imp
) {
2618 final FileInfo fi
= imp
.getOriginalFileInfo();
2619 if (null == fi
.fileName
|| fi
.fileName
.equals("")) {
2620 fi
.fileName
= "img_" + System
.currentTimeMillis() + ".tif";
2622 if (!fi
.fileName
.endsWith(".tif")) fi
.fileName
+= ".tif";
2623 fi
.directory
= dir_storage
;
2624 if (imp
.getNSlices() > 1) {
2625 new FileSaver(imp
).saveAsTiffStack(dir_storage
+ fi
.fileName
);
2627 new FileSaver(imp
).saveAsTiff(dir_storage
+ fi
.fileName
);
2629 Utils
.log2("Saved a copy into the storage folder:\n" + dir_storage
+ fi
.fileName
);
2630 return dir_storage
+ fi
.fileName
;
2633 /** Generates layer-wise mipmaps with constant tile width and height. The mipmaps include only images.
2634 * Mipmaps area generated all the way down until the entire canvas fits within one single tile.
2636 public Bureaucrat
generateLayerMipMaps(final Layer
[] la
, final int starting_level
) {
2637 // hard-coded dimensions for layer mipmaps.
2638 final int WIDTH
= 512;
2639 final int HEIGHT
= 512;
2641 // Each tile needs some coding system on where it belongs. For example in its file name, such as <layer_id>_Xi_Yi
2643 // Generate the starting level mipmaps, and then the others from it by gaussian or whatever is indicated in the project image_resizing_mode property.
2647 /** Convert old-style storage folders to new style. */
2648 public boolean fixStorageFolders() {
2650 // 1 - Create folder unuid_folder at storage_folder + unuid
2651 if (null == this.unuid
) {
2652 Utils
.log2("No unuid for project!");
2655 // the trakem2.<unuid> folder that will now contain trakem2.mipmaps, trakem2.masks, etc.
2656 final String unuid_folder
= getUNUIdFolder();
2657 File fdir
= new File(unuid_folder
);
2658 if (!fdir
.exists()) {
2659 if (!fdir
.mkdir()) {
2660 Utils
.log2("Could not create folder " + unuid_folder
);
2664 // 2 - Create trakem2.mipmaps inside unuid folder
2665 final String new_dir_mipmaps
= unuid_folder
+ "trakem2.mipmaps/";
2666 fdir
= new File(new_dir_mipmaps
);
2667 if (!fdir
.mkdir()) {
2668 Utils
.log2("Could not create folder " + new_dir_mipmaps
);
2671 // 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.
2672 final String dir_mipmaps
= getMipMapsFolder();
2673 for (final String name
: new File(dir_mipmaps
).list()) {
2674 String level_dir
= new StringBuffer(dir_mipmaps
).append(name
).append('/').toString();
2675 final File f
= new File(level_dir
);
2676 if (!f
.isDirectory() || f
.isHidden()) continue;
2677 for (final String mm
: f
.list()) {
2678 if (!mm
.endsWith(".jpg")) continue;
2679 // parse the mipmap file: filename + '.' + id + '.jpg'
2680 int last_dot
= mm
.lastIndexOf('.');
2681 if (-1 == last_dot
) continue;
2682 int prev_last_dot
= mm
.lastIndexOf('.', last_dot
-1);
2683 String id
= mm
.substring(prev_last_dot
+1, last_dot
);
2684 String filename
= mm
.substring(0, prev_last_dot
);
2685 File oldf
= new File(level_dir
+ mm
);
2686 File newf
= new File(new StringBuffer(new_dir_mipmaps
).append(name
).append('/').append(createIdPath(id
, filename
, ".jpg")).toString());
2687 File fd
= newf
.getParentFile();
2690 Utils
.log2("Could not create parent dir " + fd
.getAbsolutePath());
2693 if (!oldf
.renameTo(newf
)) {
2694 Utils
.log2("Could not move mipmap file " + oldf
.getAbsolutePath() + " to " + newf
.getAbsolutePath());
2700 this.dir_mipmaps
= new_dir_mipmaps
;
2702 // Remove old empty dirs:
2703 Utils
.removeFile(new File(dir_mipmaps
));
2705 // 4 - same for alpha folder and features folder.
2706 final String masks_folder
= getStorageFolder() + "trakem2.masks/";
2707 File fmasks
= new File(masks_folder
);
2708 this.dir_masks
= null;
2709 if (fmasks
.exists()) {
2710 final String new_dir_masks
= unuid_folder
+ "trakem2.masks/";
2711 for (final File fmask
: fmasks
.listFiles()) {
2712 final String name
= fmask
.getName();
2713 if (!name
.endsWith(".zip")) continue;
2714 int last_dot
= name
.lastIndexOf('.');
2715 if (-1 == last_dot
) continue;
2716 int prev_last_dot
= name
.lastIndexOf('.', last_dot
-1);
2717 String id
= name
.substring(prev_last_dot
+1, last_dot
);
2718 String filename
= name
.substring(0, prev_last_dot
);
2719 File newf
= new File(new_dir_masks
+ createIdPath(id
, filename
, ".zip"));
2720 File fd
= newf
.getParentFile();
2723 Utils
.log2("Could not create parent dir " + fd
.getAbsolutePath());
2726 if (!fmask
.renameTo(newf
)) {
2727 Utils
.log2("Could not move mask file " + fmask
.getAbsolutePath() + " to " + newf
.getAbsolutePath());
2732 this.dir_masks
= new_dir_masks
;
2734 // remove old empty:
2735 Utils
.removeFile(fmasks
);
2738 // TODO should save the .xml file, so the unuid and the new storage folders are set in there!
2741 } catch (Exception e
) {
2747 /** For Patch id=12345 creates 12/34/5.${filename}.jpg */
2748 static public final String
createMipMapRelPath(final Patch p
) {
2749 return createIdPath(Long
.toString(p
.getId()), new File(p
.getCurrentPath()).getName(), ".jpg");
2752 /** For sid=12345 creates 12/34/5.${filename}.jpg
2753 * Will be fine with other filename-valid chars in sid. */
2754 static public final String
createIdPath(final String sid
, final String filename
, final String ext
) {
2755 final StringBuffer sf
= new StringBuffer(((sid
.length() * 3) / 2) + 1);
2756 final int len
= sid
.length();
2757 for (int i
=1; i
<=len
; i
++) {
2758 sf
.append(sid
.charAt(i
-1));
2759 if (0 == i
% 2 && len
!= i
) sf
.append('/');
2761 return sf
.append('.').append(filename
).append(ext
).toString();
2764 public String
getUNUId() {