Updated copyright dates.
[trakem2.git] / ini / trakem2 / persistence / FSLoader.java
blob0b62c63c9679441348ec2ca5a5fc1d6a854dddfc
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 /s published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2.persistence;
25 import ij.IJ;
26 import ij.ImagePlus;
27 import ij.VirtualStack; // only after 1.38q
28 import ij.io.*;
29 import ij.process.ByteProcessor;
30 import ij.process.ImageProcessor;
31 import ij.process.FloatProcessor;
32 import ij.process.ColorProcessor;
33 import ini.trakem2.Project;
34 import ini.trakem2.ControlWindow;
35 import ini.trakem2.display.DLabel;
36 import ini.trakem2.display.Display;
37 import ini.trakem2.display.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;
56 import java.io.File;
57 import java.io.FileInputStream;
58 import java.io.FilenameFilter;
59 import java.io.InputStream;
60 import java.util.*;
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. */
112 public FSLoader() {
113 super(); // register
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) {
122 this();
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);
128 } else {
129 this.unuid = createUNUId(this.dir_storage);
130 createMipMapsDir(this.dir_storage);
131 crashDetector();
135 private String createUNUId(String dir_storage) {
136 synchronized (db_lock) {
137 lock();
138 try {
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()))
143 .toString();
144 } catch (Exception e) {
145 IJError.print(e);
146 } finally {
147 unlock();
150 return null;
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) {
155 this();
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.");
166 return;
168 File f = new File(dir_mipmaps + ".open.t2");
169 Utils.log2("Crash detector file is " + dir_mipmaps + ".open.t2");
170 try {
171 if (f.exists()) {
172 // crashed!
173 askAndExecMipmapRegeneration("TrakEM2 detected a crash!");
174 } else {
175 if (!f.createNewFile() && !dir_mipmaps.startsWith("http:")) {
176 Utils.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
177 } else {
178 Utils.log2("Created crash detection system.");
181 } catch (Exception e) {
182 Utils.log2("Crash detector error:" + e);
183 IJError.print(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;
207 else {
208 try {
209 f.mkdirs();
210 dir_image_storage = s;
211 } catch (Exception e) {
212 e.printStackTrace();
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)
223 if (null != path) {
224 path = path.replace('\\','/');
225 path = path.trim();
226 int itwo = path.indexOf("//");
227 while (-1 != itwo) {
228 if (0 == itwo /* samba disk */
229 || (5 == itwo && "http:".equals(path.substring(0, 5)))) {
230 // do nothing
231 } else {
232 path = path.substring(0, itwo) + path.substring(itwo+1);
234 itwo = path.indexOf("//", itwo+1);
238 if (null == path) {
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);
247 } else {
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.");
255 return null;
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()) {
265 handler = null;
266 } else {
267 try {
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();
273 } else {
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);
281 handler = null;
282 } catch (Exception e) {
283 IJError.print(e);
284 handler = null;
285 } finally {
286 setMassiveMode(false);
287 if (null != i_stream) {
288 try {
289 i_stream.close();
290 } catch (Exception e) {
291 IJError.print(e);
296 if (null == handler) {
297 Utils.showMessage("Error when reading the project .xml file.");
298 return null;
301 data = handler.getProjectData(open_displays);
304 if (null == data) {
305 Utils.showMessage("Error when parsing the project .xml file.");
306 return null;
308 // else, good
309 super.v_loaders.add(this);
310 crashDetector();
311 return data;
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]);
324 return null;
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();
338 // 1 core = 1 thread
339 // 2 cores = 2 threads
340 // 3+ cores = cores-1 threads
341 if (np > 2) np -= 1;
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() {
358 super.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;
377 return true;
379 }).length) {
380 try { f.delete(); } catch (Exception e) { Utils.log("Could not remove empty trakem2.mipmaps directory."); }
383 // remove crash detector
384 try {
385 File fm = new File(dir_mipmaps + ".open.t2");
386 if (!fm.delete()) {
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.");
391 IJError.print(e);
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);
404 // Only if empty:
405 if (f.isDirectory()) {
406 try {
407 if (!f.delete()) {
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() {
419 long nid = -1;
420 synchronized (db_lock) {
421 lock();
422 nid = ++max_id;
423 unlock();
425 return nid;
428 /** Loaded in full from XML file */
429 public double[][][] fetchBezierArrays(long id) {
430 return null;
433 /** Loaded in full from XML file */
434 public ArrayList fetchPipePoints(long id) {
435 return null;
438 /** Loaded in full from XML file */
439 public ArrayList fetchBallPoints(long id) {
440 return null;
443 /** Loaded in full from XML file */
444 public Area fetchArea(long area_list_id, long layer_id) {
445 return null;
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;
465 String slice = null;
466 String path = null;
467 long n_bytes = 0;
468 PatchLoadingLock plock = null;
469 synchronized (db_lock) {
470 lock();
471 imp = imps.get(p.getId());
472 try {
473 path = getAbsolutePath(p);
474 int i_sl = -1;
475 if (null != path) i_sl = path.lastIndexOf("-----#slice=");
476 if (-1 != i_sl) {
477 // activate proper slice
478 if (null != imp) {
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());
485 imp = null;
486 } else {
487 imp.setSlice(ia);
488 switch (format) {
489 case Layer.IMAGEPROCESSOR:
490 ip = imp.getStack().getProcessor(ia);
491 unlock();
492 return ip;
493 case Layer.IMAGEPLUS:
494 unlock();
495 return imp;
496 default:
497 Utils.log("FSLoader.fetchImage: Unknown format " + format);
498 return null;
501 } else {
502 unlock();
503 return null; // beyond bonds!
507 // for non-stack images
508 if (null != imp) {
509 unlock();
510 switch (format) {
511 case Layer.IMAGEPROCESSOR:
512 return imp.getProcessor();
513 case Layer.IMAGEPLUS:
514 return imp;
515 default:
516 Utils.log("FSLoader.fetchImage: Unknown format " + format);
517 return null;
520 if (-1 != i_sl) {
521 slice = path.substring(i_sl);
522 // set path proper
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) {
529 IJError.print(e);
530 return null;
531 } finally {
532 unlock();
537 synchronized (plock) {
538 plock.lock();
540 imp = imps.get(p.getId());
541 if (null != imp) {
542 // was loaded by a different thread -- TODO the slice of the stack could be wrong!
543 plock.unlock();
544 switch (format) {
545 case Layer.IMAGEPROCESSOR:
546 return imp.getProcessor();
547 case Layer.IMAGEPLUS:
548 return imp;
549 default:
550 Utils.log("FSLoader.fetchImage: Unknown format " + format);
551 return null;
555 // going to load:
558 // reserve memory:
559 synchronized (db_lock) {
560 lock();
561 n_bytes = estimateImageFileSize(p, 0);
562 max_memory -= n_bytes;
563 unlock();
566 releaseToFit(n_bytes);
567 imp = openImage(path);
569 preProcess(imp);
571 synchronized (db_lock) {
572 try {
573 lock();
574 max_memory += n_bytes;
576 if (null == imp) {
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);
585 unlock();
586 plock.unlock();
587 return null;
589 // update all clients of the stack, if any
590 if (null != slice) {
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=");
603 if (-1 != isl) {
604 //int i_slice = Integer.parseInt(str.substring(isl + 12));
605 final long lid = entry.getKey();
606 imps.put(lid, imp);
609 // set proper active slice
610 final int ia = Integer.parseInt(slice.substring(12));
611 imp.setSlice(ia);
612 if (Layer.IMAGEPROCESSOR == format) ip = imp.getStack().getProcessor(ia); // otherwise creates one new for nothing
613 } else {
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) {
624 IJError.print(e);
626 unlock();
627 plock.unlock();
628 switch (format) {
629 case Layer.IMAGEPROCESSOR:
630 return ip; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
631 case Layer.IMAGEPLUS:
632 return imp;
633 default:
634 Utils.log("FSLoader.fetchImage: Unknown format " + format);
635 return null;
642 /** Returns the alpha mask image from a file, or null if none stored. */
643 @Override
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);
650 if (null == imp) {
651 //Utils.log2("No mask found or could not open mask image for patch " + p + " from " + path);
652 return null;
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());
658 return null;
660 return mask;
663 @Override
664 public String getAlphaPath(final Patch p) {
665 final String filename = getInternalFileName(p);
666 if (null == filename) {
667 Utils.log2("null filepath!");
668 return null;
670 final String dir = getMasksFolder();
671 return new StringBuffer(dir).append(createIdPath(Long.toString(p.getId()), filename, ".zip")).toString();
674 @Override
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();
679 parent.mkdirs();
680 IJ.redirectErrorMessages();
681 new FileSaver(new ImagePlus("mask", fp)).saveAsZip(getAlphaPath(p));
684 public final String getMasksFolder() {
685 if (null == dir_masks) createMasksFolder();
686 return dir_masks;
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;
693 try {
694 f.mkdirs();
695 } catch (Exception e) {
696 IJError.print(e);
700 /** Remove the file containing the given Patch's alpha mask. */
701 public final boolean removeAlphaMask(final Patch p) {
702 try {
703 File f = new File(getAlphaPath(p));
704 if (f.exists()) {
705 return f.delete();
707 return true;
708 } catch (Exception e) {
709 IJError.print(e);
711 return false;
714 /** Loaded in full from XML file */
715 public Object[] fetchLabel(DLabel label) {
716 return null;
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);
725 // reserve memory:
726 synchronized (db_lock) {
727 lock();
728 max_memory -= n_bytes;
729 unlock();
731 try {
732 return openImage(original_path);
733 } catch (Throwable t) {
734 IJError.print(t);
735 } finally {
736 synchronized (db_lock) {
737 lock();
738 max_memory += n_bytes;
739 unlock();
742 return null;
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) {
755 lock();
756 setChanged(true);
757 final long id = ob.getId();
758 if (id > max_id) {
759 max_id = id;
761 unlock();
763 return true;
766 public boolean updateInDatabase(final DBObject ob, final String key) {
767 // Should only be GUI-driven
768 setChanged(true);
770 if (ob.getClass() == Patch.class) {
771 Patch p = (Patch)ob;
772 if (key.equals("tiff_working")) return null != setImageFile(p, fetchImagePlus(p));
774 return true;
777 public boolean removeFromDatabase(final DBObject ob) {
778 synchronized (db_lock) {
779 lock();
780 setChanged(true);
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.
786 Patch p = (Patch)ob;
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);
791 if (null != imp) {
792 if (imp.getStackSize() > 1) {
793 if (null == imp.getProcessor()) {}
794 else if (null == imp.getProcessor().getPixels()) {}
795 else Loader.flush(imp); // only once
796 } else {
797 Loader.flush(imp);
800 cannot_regenerate.remove(p);
801 unlock();
802 flushMipMaps(p.getId()); // locks on its own
803 touched_mipmaps.remove(p);
804 return true;
806 unlock();
808 return true;
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;
816 try {
817 String path = getAbsolutePath(p);
818 String slice = null;
820 // path can be null if the image is pasted, or from a copy, or totally new
821 if (null != path) {
822 int i_sl = path.lastIndexOf("-----#slice=");
823 if (-1 != i_sl) {
824 slice = path.substring(i_sl);
825 path = path.substring(0, i_sl);
827 } else {
828 // no path, inspect image FileInfo's path if the image has no changes
829 if (!imp.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);
836 cacheAll(p, imp);
837 Utils.log2("Reusing image file: path exists for fileinfo at " + fipath);
838 return fipath;
843 if (null != path) {
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
860 filename += "tif";
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
868 File file = null;
869 int i = 1;
870 final int itag = path.lastIndexOf(tag);
871 do {
872 path = path.substring(0, itag) + "." + i + tag + "tif";
873 i++;
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
885 if (null != path2) {
886 updatePaths(p, path2, null != slice);
887 cacheAll(p, imp);
888 hs_unloadable.remove(p);
889 return path2;
890 } else {
891 Utils.log("WARNING could not save image at " + path);
892 // undo
893 updatePaths(p, starting_path, null != slice);
894 return null;
897 } catch (Exception e) {
898 IJError.print(e);
900 return null;
904 * TODO
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();
912 return title;
915 /** Associate patch with imp, and all slices as well if any. */
916 private void cacheAll(final Patch p, final ImagePlus imp) {
917 if (p.isStack()) {
918 for (Patch pa : p.getStackPatches()) {
919 cache(pa, imp);
921 } else {
922 cache(p, imp);
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) {
929 lock();
930 try {
931 // ensure the old path is cached in the Patch, to get set as the original if there is no original.
932 if (is_stack) {
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));
939 } else {
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) {
946 IJError.print(e);
947 } finally {
948 unlock();
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=");
962 String slice = null;
963 if (-1 != i_sl) {
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);
972 return null;
974 // else assume that it exists
976 // reappend slice info if existent
977 if (null != slice) path += slice;
978 // set it
979 patch.cacheCurrentPath(path);
980 return 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) {
1003 if (null == path) {
1004 Utils.log("Null path for patch: " + patch);
1005 return;
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);
1025 // store
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;
1041 else {
1042 this.project_file_path = xml_path;
1043 ControlWindow.updateTitle(project);
1044 result = this.project_file_path;
1046 } else {
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();
1054 return result;
1057 public String saveAs(Project project) {
1058 String path = super.saveAs(project, null, false);
1059 if (null != path) {
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);
1065 return path;
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) {
1070 if (null == path) {
1071 Utils.log("Cannot save on null path.");
1072 return null;
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);
1084 if (!overwrite) {
1085 int i = 1;
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);
1093 i++;
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();
1104 return path2;
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) {
1115 //unsaved project
1116 return path;
1118 if (null == path) {
1119 return null;
1121 // fix W1nd0ws paths
1122 path = path.replace('\\', '/'); // char-based, no parsing problems
1123 // remove slice tag
1124 String slice = null;
1125 int isl = path.lastIndexOf("-----#slice");
1126 if (-1 != isl) {
1127 slice = path.substring(isl);
1128 path = path.substring(0, isl);
1131 if (isRelativePath(path)) {
1132 // already relative
1133 if (-1 != isl) path += slice;
1134 return path;
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);
1148 return 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")) {
1157 save(project);
1158 } else if (command.equals("Save as...")) {
1159 saveAs(project);
1163 JMenuItem item;
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;
1173 if (as_copy) {
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('/')) {
1178 target_dir += "/";
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.");
1197 return null;
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);
1209 releaseMemory();
1210 Utils.log2(i + " : " + patch_path);
1211 imp_patch_i = openImage(patch_path);
1212 } else {
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 = "";
1221 Patch patch = null;
1222 if (as_copy) {
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);
1228 } else {
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);
1245 layer.add(patch);
1246 previous_patch = patch;
1247 Utils.showProgress(i * (1.0 / n));
1249 Utils.showProgress(1.0);
1251 // update calibration
1252 // TODO
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");
1261 if (null != ob) {
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");
1268 if (null != ob) {
1269 String sf = ((String)ob).replace('\\', '/');
1270 if (isRelativePath(sf)) {
1271 sf = getParentFolder() + sf;
1273 if (isURL(sf)) {
1274 // can't be an URL
1275 Utils.log2("Can't have an URL as the path of a storage folder.");
1276 } else {
1277 File f = new File(sf);
1278 if (f.exists() && f.isDirectory()) {
1279 this.dir_storage = sf;
1280 } else {
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('\\', '/');
1299 // fix
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");
1304 if (null != ob) {
1305 String mf = ((String)ob).replace('\\', '/');
1306 if (isRelativePath(mf)) {
1307 mf = getParentFolder() + mf;
1309 if (isURL(mf)) {
1310 this.dir_mipmaps = mf;
1311 // TODO must disable input somehow, so that images are not edited.
1312 } else {
1313 File f = new File(mf);
1314 if (f.exists() && f.isDirectory()) {
1315 this.dir_mipmaps = mf;
1316 } else {
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);
1337 // fix
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;
1356 new Thread() {
1357 public void run() {
1358 try {
1359 // wait while parsing the rest of the XML file
1360 while (!v_loaders.contains(lo)) {
1361 Thread.sleep(1000);
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) {}
1370 }.start();
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() {
1391 return dir_mipmaps;
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];
1402 c[255] = (byte)255;
1403 thresh_cm = new IndexColorModel(8, 256, c, c, c);
1405 return thresh_cm;
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);
1412 try {
1413 pg.grabPixels();
1414 return pg.getPixels();
1415 } catch (InterruptedException e) {
1416 IJError.print(e);
1418 return null;
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);
1448 return thresholded;
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;
1455 //else:
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);
1458 return bi;
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);
1466 } else {
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
1472 img.flush();
1474 return bi;
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) {
1479 BufferedImage bi;
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);
1489 } else {
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());
1513 the_alpha.flush();
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) {
1532 switch (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:
1540 default:
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];
1556 int rs = 0;
1557 for (int r = 0; r < targetData.length; r += targetWidth) {
1558 int xs = -1;
1559 for (int x = 0; x < targetWidth; ++x)
1560 targetData[r + x] = sourceData[rs + ++xs] + sourceData[rs + ++xs];
1561 rs += sourceWidth;
1562 xs = -1;
1563 for (int x = 0; x < targetWidth; ++x) {
1564 targetData[r + x] += sourceData[rs + ++xs] + sourceData[rs + ++xs];
1565 targetData[r + x] /= 4;
1567 rs += sourceWidth;
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);
1598 if (null == path) {
1599 Utils.log2("generateMipMaps: cannot find path for Patch " + patch);
1600 cannot_regenerate.add(patch);
1601 return false;
1603 if (hs_unloadable.contains(patch)) {
1604 FilePathRepair.add(patch);
1605 return false;
1607 synchronized (gm_lock) {
1608 try {
1609 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);
1615 return false;
1617 hs_regenerating_mipmaps.add(patch);
1618 } catch (Exception e) {
1619 IJError.print(e);
1620 } finally {
1621 gm_unlock();
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);
1638 try {
1639 // Now:
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
1643 ImageProcessor ip;
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();
1651 if (null == pai) {
1652 Utils.log("Can't regenerate mipmaps for patch " + patch);
1653 cannot_regenerate.add(patch);
1654 return false;
1656 ip = pai.target;
1657 alpha_mask = pai.mask; // can be null
1658 outside_mask = pai.outside; // can be null
1659 coordinate_transformed = pai.coordinate_transformed;
1660 pai = null;
1662 // Old style:
1663 //final String filename = new StringBuffer(new File(path).getName()).append('.').append(patch.getId()).append(".jpg").toString();
1664 // New style:
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) {
1683 switch (type) {
1684 case ImagePlus.COLOR_RGB:
1685 case ImagePlus.COLOR_256:
1686 case ImagePlus.GRAY8:
1687 patch.setMinAndMax(0, 255);
1688 break;
1689 // Find and flow through to default:
1690 case ImagePlus.GRAY16:
1691 ((ij.process.ShortProcessor)ip).findMinAndMax();
1692 patch.setMinAndMax(ip.getMin(), ip.getMax());
1693 break;
1694 case ImagePlus.GRAY32:
1695 ((FloatProcessor)ip).findMinAndMax();
1696 patch.setMinAndMax(ip.getMin(), ip.getMax());
1697 break;
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();
1710 cm = null;
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());
1725 } else {
1726 alpha = null;
1728 if (null != outside_mask) {
1729 outside = new FloatProcessorT2((FloatProcessor)outside_mask.convertToFloat());
1730 if ( null == alpha ) {
1731 alpha = outside;
1732 alpha_mask = outside_mask;
1734 } else {
1735 outside = null;
1738 // sw,sh are the dimensions of the image to blur
1739 // w,h are the dimensions to scale the blurred image to
1740 int sw = w,
1741 sh = h;
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);
1755 } else {
1756 do {
1757 if (Thread.currentThread().isInterrupted()) return false;
1758 // 1 - Prepare values for the next scaled image
1759 sw = w;
1760 sh = h;
1761 w /= 2;
1762 h /= 2;
1763 k++;
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 ) {
1773 final byte[] o;
1774 if (alpha != outside)
1775 o = gaussianBlurResizeInHalf(outside, sw, sh, w, h); // idem
1776 else
1777 o = a;
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);
1792 // 5 - Save as jpeg
1793 if (!ini.trakem2.io.ImageSaver.saveAsJpeg(cp2, target_dir + filename, 0.85f, false)) {
1794 cannot_regenerate.add(patch);
1795 break;
1797 } else {
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);
1805 bi_save.flush();
1806 break;
1808 bi_save.flush();
1810 } while (w >= 32 && h >= 32); // not smaller than 32x32
1812 } else {
1813 // Greyscale:
1814 releaseToFit(w * h * 4 * 5);
1815 final boolean as_grey = !ip.isColorLut();
1816 if (as_grey && null == cm) {
1817 cm = GRAY_LUT;
1820 if (Thread.currentThread().isInterrupted()) return false;
1822 if (Loader.GAUSSIAN == resizing_mode) {
1823 FloatProcessor fp = (FloatProcessor) ip.convertToFloat();
1824 int sw=w, sh=h;
1826 FloatProcessor alpha;
1827 FloatProcessor outside;
1828 if (null != alpha_mask) {
1829 alpha = new FloatProcessorT2((FloatProcessor)alpha_mask.convertToFloat());
1830 } else {
1831 alpha = null;
1833 if (null != outside_mask) {
1834 outside = new FloatProcessorT2((FloatProcessor)outside_mask.convertToFloat());
1835 if (null == alpha) {
1836 alpha = outside;
1837 alpha_mask = outside_mask;
1839 } else {
1840 outside = null;
1843 do {
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
1863 if (0 != k) {
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.
1867 } else {
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) {
1881 final byte[] o;
1882 if (alpha != outside) {
1883 o = (byte[])outside.convertToByte(false).getPixels();
1884 } else {
1885 o = a;
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);
1901 bi_save.flush();
1902 break;
1904 bi_save.flush();
1905 } else {
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);
1913 break;
1917 // 4 - prepare values for the next scaled image
1918 sw = w;
1919 sh = h;
1920 w /= 2;
1921 h /= 2;
1922 k++;
1923 } while (w >= 32 && h >= 32); // not smaller than 32x32
1925 } else {
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);
1937 ip = null;
1939 do {
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);
1948 bi = res[0];
1949 balpha = res[1];
1950 boutside = res[2];
1951 // prepare next iteration
1952 if (awt != bi) awt.flush();
1953 awt = bi;
1954 w /= 2;
1955 h /= 2;
1956 k++;
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);
1962 break;
1964 } while (w >= 32 && h >= 32);
1965 bi.flush();
1967 //timer.cumulative();
1971 // flush any cached tiles
1972 flushMipMaps(patch.getId());
1974 return true;
1975 } catch (Throwable e) {
1976 IJError.print(e);
1977 cannot_regenerate.add(patch);
1978 return false;
1979 } finally {
1980 // gets executed even when returning from the catch statement or within the try/catch block
1981 synchronized (gm_lock) {
1982 gm_lock();
1983 hs_regenerating_mipmaps.remove(patch);
1984 gm_unlock();
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());
1993 if (f.exists()) {
1994 try {
1995 return f.delete();
1996 } catch (Exception e) {
1997 IJError.print(e);
1998 return false;
2000 } else return true;
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);
2020 } else {
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),
2035 removed_paths);
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");
2044 if (idot < 0) {
2045 Utils.log2("Not a pointmatches.ser file: can't process " + path);
2046 continue;
2049 // Find the root
2050 int ifolder = path.indexOf("pointmatches.ser/");
2051 if (ifolder < 0) {
2052 Utils.log2("Not in pointmatches.ser/ folder:" + path);
2053 continue;
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);
2066 continue;
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);
2071 if (f.exists()) {
2072 if (!f.delete()) {
2073 Utils.log2("Could not delete " + f.getAbsolutePath());
2074 success = false;
2075 } else {
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
2084 break;
2086 islash = dirname.lastIndexOf('/');
2089 } else {
2090 Utils.log2("File does not exist: " + dir + name);
2094 return success;
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.)
2101 * */
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.");
2107 return null;
2109 final Worker worker = new Worker("Generating MipMaps") {
2110 public void run() {
2111 this.setAsBackground(true);
2112 this.startedWorking();
2113 try {
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();
2122 al.toArray(pa);
2123 final AtomicInteger ai = new AtomicInteger(0);
2125 for (int ithread = 0; ithread < threads.length; ++ithread) {
2126 threads[ithread] = new Thread(new Runnable() {
2127 public void run() {
2129 for (int k = ai.getAndIncrement(); k < size; k = ai.getAndIncrement()) {
2130 if (wo.hasQuitted()) {
2131 return;
2133 wo.setTaskName("Generating MipMaps " + (k+1) + "/" + size);
2134 try {
2135 boolean ow = overwrite;
2136 if (!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();
2140 int level = 0;
2141 final String filename = new File(getAbsolutePath(pa[k])).getName() + "." + pa[k].getId() + ".jpg";
2142 do {
2143 w /= 2;
2144 h /= 2;
2145 level++;
2146 if (!new File(dir_mipmaps + level + "/" + filename).exists()) {
2147 ow = true;
2148 break;
2150 } while (w >= 32 && h >= 32);
2152 if (!ow) continue;
2153 if ( ! generateMipMaps(pa[k]) ) {
2154 // some error ocurred
2155 Utils.log2("Could not generate mipmaps for patch " + pa[k]);
2157 } catch (Exception e) {
2158 IJError.print(e);
2165 MultiThreading.startAndJoin(threads);
2167 } catch (Exception e) {
2168 IJError.print(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) {
2180 lock();
2181 final String path = new StringBuffer(dir_mipmaps).append(level).append('/').toString();
2182 if (isURL(dir_mipmaps)) {
2183 unlock();
2184 return path;
2186 final File file = new File(path);
2187 if (file.exists() && file.isDirectory()) {
2188 unlock();
2189 return path;
2191 // else, create it
2192 try {
2193 file.mkdir();
2194 unlock();
2195 return path;
2196 } catch (Exception e) {
2197 IJError.print(e);
2199 unlock();
2201 return null;
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();
2222 } else {
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++) {
2229 try {
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);
2239 this.unuid = unuid;
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/";
2249 return unuid_dir;
2252 return null;
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");
2262 if (!f.exists()) {
2263 try {
2264 if (f.mkdir()) {
2265 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
2266 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
2267 return true;
2269 } catch (Exception e) {}
2270 } else if (f.isDirectory()) {
2271 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
2272 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
2273 return true;
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()) {
2288 // all OK
2289 this.dir_mipmaps = parent_path + "trakem2." + unuid + "/trakem2.mipmaps/";
2290 try {
2291 File f = new File(this.dir_mipmaps);
2292 f.mkdirs();
2293 if (!f.exists()) {
2294 Utils.log("Could not create trakem2.mipmaps!");
2295 return false;
2297 } catch (Exception e) {
2298 IJError.print(e);
2299 return false;
2301 } else {
2302 Utils.showMessage("Selected parent path is not a directory. Please choose another one.");
2303 return createMipMapsDir(null);
2305 } else {
2306 Utils.showMessage("Parent path does not exist. Please select a new one.");
2307 return createMipMapsDir(null);
2309 return true;
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) {
2316 lock();
2317 if (forget_dir_mipmaps) this.dir_mipmaps = null;
2318 mawts.removeAllPyramids(); // does not remove level 0 awts (i.e. the 100% images)
2319 unlock();
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) {
2327 lock();
2328 try {
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(); }
2335 unlock();
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;
2342 try {
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);
2351 }});
2352 } catch (Exception e) {
2353 IJError.print(e);
2357 private void removeMipMaps(final String filename, final int width, final int height) {
2358 int w = width;
2359 int h = height;
2360 int k = 0; // the level
2361 do {
2362 final File f = new File(dir_mipmaps + k + "/" + filename);
2363 if (f.exists()) {
2364 try {
2365 if (!f.delete()) {
2366 Utils.log2("Could not remove file " + f.getAbsolutePath());
2368 } catch (Exception e) {
2369 IJError.print(e);
2372 w /= 2;
2373 h /= 2;
2374 k++;
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;
2388 try {
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;
2398 return level;
2399 } else {
2400 do {
2401 final File f = new File(new StringBuffer(dir_mipmaps).append(level).append('/').append(filename).toString());
2402 if (f.exists()) {
2403 return level;
2405 // try the next level
2406 level--;
2407 } while (level >= 0);
2409 } catch (Exception e) {
2410 IJError.print(e);
2412 return 0;
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) {} }
2425 gm_locked = true;
2427 protected final void gm_unlock() {
2428 //Utils.printCaller(this, 7);
2429 if (gm_locked) {
2430 gm_locked = false;
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;
2441 return false;
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");
2450 return null;
2452 try {
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!");
2463 return null;
2465 // Old style:
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();
2467 // New style:
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();
2470 Image img = null;
2472 if (patch.hasAlphaChannel()) {
2473 img = ImageSaver.openJpegAlpha(path);
2474 } else {
2475 switch (patch.getType()) {
2476 case ImagePlus.GRAY16:
2477 case ImagePlus.GRAY8:
2478 case ImagePlus.GRAY32:
2479 img = ImageSaver.openGreyJpeg(path);
2480 break;
2481 default:
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());
2489 break;
2492 if (null != img) return img;
2495 // if we got so far ... try to regenerate the mipmaps
2496 if (!mipmaps_regen) {
2497 return null;
2500 // check that REALLY the file doesn't exist.
2501 if (cannot_regenerate.contains(patch)) {
2502 Utils.log("Cannot regenerate mipmaps for patch " + patch);
2503 return null;
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()) {
2511 // regenerate
2512 regenerateMipMaps(patch);
2513 return REGENERATING;
2515 } catch (Exception e) {
2516 IJError.print(e);
2518 return null;
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) {
2529 try {
2530 gm_lock();
2531 if (hs_regenerating_mipmaps.contains(patch)) {
2532 return null;
2534 // else, start it
2535 hs_regenerating_mipmaps.add(patch);
2536 } catch (Exception e) {
2537 IJError.print(e);
2538 return null;
2539 } finally {
2540 gm_unlock();
2543 try {
2544 n_regenerating.incrementAndGet();
2545 Utils.log2("SUBMITTING to regen " + patch);
2546 return regenerator.submit(new Runnable() {
2547 public void run() {
2548 try {
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) {
2554 IJError.print(e);
2556 n_regenerating.decrementAndGet();
2559 } catch (Exception e) {
2560 IJError.print(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());
2569 return null;
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) {
2574 if (level > 0) {
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();
2582 switch (type) {
2583 case ImagePlus.GRAY32:
2584 bytes_per_pixel = 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
2585 break;
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;
2590 break;
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[]
2597 break;
2598 default:
2599 bytes_per_pixel = 5; // conservative
2600 break;
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);
2626 } else {
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.
2644 return null;
2647 /** Convert old-style storage folders to new style. */
2648 public boolean fixStorageFolders() {
2649 try {
2650 // 1 - Create folder unuid_folder at storage_folder + unuid
2651 if (null == this.unuid) {
2652 Utils.log2("No unuid for project!");
2653 return false;
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);
2661 return false;
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);
2669 return false;
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();
2688 fd.mkdirs();
2689 if (!fd.exists()) {
2690 Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
2691 continue;
2693 if (!oldf.renameTo(newf)) {
2694 Utils.log2("Could not move mipmap file " + oldf.getAbsolutePath() + " to " + newf.getAbsolutePath());
2695 continue;
2699 // Set it!
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();
2721 fd.mkdirs();
2722 if (!fd.exists()) {
2723 Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
2724 continue;
2726 if (!fmask.renameTo(newf)) {
2727 Utils.log2("Could not move mask file " + fmask.getAbsolutePath() + " to " + newf.getAbsolutePath());
2728 continue;
2731 // Set it!
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!
2740 return true;
2741 } catch (Exception e) {
2742 IJError.print(e);
2744 return false;
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() {
2765 return unuid;