Attempt to ease the pain for multiple projects sharing same mipmaps folder.
[trakem2.git] / ini / trakem2 / persistence / FSLoader.java
blobac29315c1d52bb4bb8207a3e35240f0e95501e33
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005, 2006 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 /s published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
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.ExecutorService;
80 import java.util.concurrent.Executors;
81 import java.util.concurrent.ThreadPoolExecutor;
82 import java.util.regex.Pattern;
85 /** A class to rely on memory only; except images which are rolled from a folder or their original location and flushed when memory is needed for more. Ideally there would be a given folder for storing items temporarily of permanently as the "project folder", but I haven't implemented it. */
86 public final class FSLoader extends Loader {
88 /** Largest id seen so far. */
89 private long max_id = -1;
90 private final HashMap<Long,String> ht_paths = new HashMap<Long,String>();
91 /** For saving and overwriting. */
92 private String project_file_path = null;
93 /** Path to the directory hosting the file image pyramids. */
94 private String dir_mipmaps = null;
95 /** Path to the directory the user provided when creating the project. */
96 private String dir_storage = null;
97 /** Path to the directory hosting the alpha masks. */
98 private String dir_masks = null;
100 /** Path to dir_storage + "trakem2.images/" */
101 private String dir_image_storage = null;
103 /** Queue and execute Runnable tasks. */
104 static private Dispatcher dispatcher = new Dispatcher();
106 private Set<Patch> touched_mipmaps = Collections.synchronizedSet(new HashSet<Patch>());
108 /** Used to open a project from an existing XML file. */
109 public FSLoader() {
110 super(); // register
111 super.v_loaders.remove(this); //will be readded on successful open
112 FSLoader.startStaticServices();
115 private String unuid = null;
117 /** Used to create a new project, NOT from an XML file. */
118 public FSLoader(final String storage_folder) {
119 this();
120 if (null == storage_folder) this.dir_storage = super.getStorageFolder(); // home dir
121 else this.dir_storage = storage_folder;
122 if (!this.dir_storage.endsWith("/")) this.dir_storage += "/";
123 if (!Loader.canReadAndWriteTo(dir_storage)) {
124 Utils.log("WARNING can't read/write to the storage_folder at " + dir_storage);
125 } else {
126 this.unuid = createUNUId(this.dir_storage);
127 createMipMapsDir(this.dir_storage);
128 crashDetector();
132 private String createUNUId(String dir_storage) {
133 synchronized (db_lock) {
134 lock();
135 try {
136 if (null == dir_storage) dir_storage = System.getProperty("user.dir") + "/";
137 return new StringBuffer(64).append(System.currentTimeMillis()).append('.')
138 .append(Math.abs(dir_storage.hashCode())).append('.')
139 .append(Math.abs(System.getProperty("user.name").hashCode()))
140 .toString();
141 } catch (Exception e) {
142 IJError.print(e);
143 } finally {
144 unlock();
147 return null;
150 /** Create a new FSLoader copying some key parameters such as preprocessor plugin, and storage and mipmap folders. Used for creating subprojects. */
151 public FSLoader(final Loader source) {
152 this();
153 this.dir_storage = source.getStorageFolder(); // can never be null
154 this.dir_mipmaps = source.getMipMapsFolder();
155 if (null == this.dir_mipmaps) createMipMapsDir(this.dir_storage);
156 setPreprocessor(source.getPreprocessor());
159 /** Store a hidden file in trakem2.mipmaps directory that means: "the project is open", which is deleted when the project is closed. If the file is present on opening a project, it means the project has not been closed properly, and some mipmaps may be wrong. */
160 private void crashDetector() {
161 if (null == dir_mipmaps) {
162 Utils.log2("Could NOT create crash detection system: null dir_mipmaps.");
163 return;
165 File f = new File(dir_mipmaps + ".open.t2");
166 Utils.log2("Crash detector file is " + dir_mipmaps + ".open.t2");
167 try {
168 if (f.exists()) {
169 // crashed!
170 askAndExecMipmapRegeneration("TrakEM2 detected a crash!");
171 } else {
172 if (!f.createNewFile() && !dir_mipmaps.startsWith("http:")) {
173 Utils.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
174 } else {
175 Utils.log2("Created crash detection system.");
178 } catch (Exception e) {
179 Utils.log2("Crash detector error:" + e);
180 IJError.print(e);
184 public String getProjectXMLPath() {
185 if (null == project_file_path) return null;
186 return project_file_path.toString(); // a copy of it
189 /** Return the folder selected by a user to store files into; it's also the parent folder of the UNUId folder, and the recommended folder to store the XML file into. */
190 public String getStorageFolder() {
191 if (null == dir_storage) return super.getStorageFolder(); // the user's home
192 return dir_storage.toString(); // a copy
195 /** Returns a folder proven to be writable for images can be stored into. */
196 public String getImageStorageFolder() {
197 if (null == dir_image_storage) {
198 String s = getUNUIdFolder() + "trakem2.images/";
199 File f = new File(s);
200 if (f.exists() && f.isDirectory() && f.canWrite()) {
201 dir_image_storage = s;
202 return dir_image_storage;
204 else {
205 try {
206 f.mkdirs();
207 dir_image_storage = s;
208 } catch (Exception e) {
209 e.printStackTrace();
210 return getStorageFolder(); // fall back
214 return dir_image_storage;
217 /** Returns TMLHandler.getProjectData() . If the path is null it'll be asked for. */
218 public Object[] openFSProject(String path, final boolean open_displays) {
219 // clean path of double-slashes, safely (and painfully)
220 if (null != path) {
221 path = path.replace('\\','/');
222 path = path.trim();
223 int itwo = path.indexOf("//");
224 while (-1 != itwo) {
225 if (0 == itwo /* samba disk */
226 || (5 == itwo && "http:".equals(path.substring(0, 5)))) {
227 // do nothing
228 } else {
229 path = path.substring(0, itwo) + path.substring(itwo+1);
231 itwo = path.indexOf("//", itwo+1);
235 if (null == path) {
236 String user = System.getProperty("user.name");
237 OpenDialog od = new OpenDialog("Select Project", OpenDialog.getDefaultDirectory(), null);
238 String file = od.getFileName();
239 if (null == file || file.toLowerCase().startsWith("null")) return null;
240 String dir = od.getDirectory().replace('\\', '/');
241 if (!dir.endsWith("/")) dir += "/";
242 this.project_file_path = dir + file;
243 Utils.log2("project file path 1: " + this.project_file_path);
244 } else {
245 this.project_file_path = path;
246 Utils.log2("project file path 2: " + this.project_file_path);
248 Utils.log2("Loader.openFSProject: path is " + path);
249 // check if any of the open projects uses the same file path, and refuse to open if so:
250 if (null != FSLoader.getOpenProject(project_file_path, this)) {
251 Utils.showMessage("The project is already open.");
252 return null;
255 Object[] data = null;
257 // parse file, according to expected format as indicated by the extension:
258 if (this.project_file_path.toLowerCase().endsWith(".xml")) {
259 InputStream i_stream = null;
260 TMLHandler handler = new TMLHandler(this.project_file_path, this);
261 if (handler.isUnreadable()) {
262 handler = null;
263 } else {
264 try {
265 SAXParserFactory factory = SAXParserFactory.newInstance();
266 factory.setValidating(true);
267 SAXParser parser = factory.newSAXParser();
268 if (isURL(this.project_file_path)) {
269 i_stream = new java.net.URL(this.project_file_path).openStream();
270 } else {
271 i_stream = new BufferedInputStream(new FileInputStream(this.project_file_path));
273 InputSource input_source = new InputSource(i_stream);
274 setMassiveMode(true);
275 parser.parse(input_source, handler);
276 } catch (java.io.FileNotFoundException fnfe) {
277 Utils.log("ERROR: File not found: " + path);
278 handler = null;
279 } catch (Exception e) {
280 IJError.print(e);
281 handler = null;
282 } finally {
283 setMassiveMode(false);
284 if (null != i_stream) {
285 try {
286 i_stream.close();
287 } catch (Exception e) {
288 IJError.print(e);
293 if (null == handler) {
294 Utils.showMessage("Error when reading the project .xml file.");
295 return null;
298 data = handler.getProjectData(open_displays);
301 if (null == data) {
302 Utils.showMessage("Error when parsing the project .xml file.");
303 return null;
305 // else, good
306 super.v_loaders.add(this);
307 crashDetector();
308 return data;
311 // Only one thread at a time may access this method.
312 synchronized static private final Project getOpenProject(final String project_file_path, final Loader caller) {
313 if (null == v_loaders) return null;
314 final Loader[] lo = (Loader[])v_loaders.toArray(new Loader[0]); // atomic way to get the list of loaders
315 for (int i=0; i<lo.length; i++) {
316 if (lo[i].equals(caller)) continue;
317 if (lo[i] instanceof FSLoader && ((FSLoader)lo[i]).project_file_path.equals(project_file_path)) {
318 return Project.findProject(lo[i]);
321 return null;
324 static public final Project getOpenProject(final String project_file_path) {
325 return getOpenProject(project_file_path, null);
328 public boolean isReady() {
329 return null != ht_paths;
332 static private void startStaticServices() {
333 if (null == dispatcher || dispatcher.isQuit()) dispatcher = new Dispatcher();
334 int np = Runtime.getRuntime().availableProcessors();
335 // 1 core = 1 thread
336 // 2 cores = 2 threads
337 // 3+ cores = cores-1 threads
338 if (np > 2) np -= 1;
339 if (null == regenerator || regenerator.isShutdown()) {
340 regenerator = Executors.newFixedThreadPool(np);
342 if (null == repainter || repainter.isShutdown()) {
343 repainter = Executors.newFixedThreadPool(np); // for SnapshotPanel
347 /** Shutdown the various thread pools and disactivate services in general. */
348 static private void destroyStaticServices() {
349 if (null != regenerator) regenerator.shutdownNow();
350 if (null != dispatcher) dispatcher.quit();
351 if (null != repainter) repainter.shutdownNow();
354 public void destroy() {
355 super.destroy();
356 Utils.showStatus("", false);
357 // delete mipmap files that where touched and not cleared as saved (i.e. the project was not saved)
358 for (final Patch p : touched_mipmaps) {
359 File f = new File(getAbsolutePath(p));
360 Utils.log2("File f is " + f);
361 if (f.exists()) { // TODO this may not work for stacks!
362 Utils.log2("Removing mipmaps for " + p);
363 // Cannot run in the dispatcher: is a daemon, and would be interrupted.
364 removeMipMaps(createIdPath(Long.toString(p.getId()), f.getName(), ".jpg"), (int)p.getWidth(), (int)p.getHeight()); // needs the dispatcher!
368 // remove empty trakem2.mipmaps folder if any
369 if (null != dir_mipmaps && !dir_mipmaps.equals(dir_storage)) {
370 File f = new File(dir_mipmaps);
371 if (f.isDirectory() && 0 == f.list(new FilenameFilter() {
372 public boolean accept(File fdir, String name) {
373 File file = new File(dir_mipmaps + name);
374 if (file.isHidden() || '.' == name.charAt(0)) return false;
375 return true;
377 }).length) {
378 try { f.delete(); } catch (Exception e) { Utils.log("Could not remove empty trakem2.mipmaps directory."); }
381 // remove crash detector
382 File f = new File(dir_mipmaps + ".open.t2");
383 try {
384 if (!f.delete()) {
385 Utils.log2("WARNING: could not delete crash detector file .open.t2 from trakem2.mipmaps folder at " + dir_mipmaps);
387 } catch (Exception e) {
388 Utils.log2("WARNING: crash detector file trakem.mipmaps/.open.t2 may NOT have been deleted.");
389 IJError.print(e);
391 if (null == ControlWindow.getProjects() || 1 == ControlWindow.getProjects().size()) {
392 destroyStaticServices();
396 /** Get the next unique id, not shared by any other object within the same project. */
397 public long getNextId() {
398 long nid = -1;
399 synchronized (db_lock) {
400 lock();
401 nid = ++max_id;
402 unlock();
404 return nid;
407 /** Loaded in full from XML file */
408 public double[][][] fetchBezierArrays(long id) {
409 return null;
412 /** Loaded in full from XML file */
413 public ArrayList fetchPipePoints(long id) {
414 return null;
417 /** Loaded in full from XML file */
418 public ArrayList fetchBallPoints(long id) {
419 return null;
422 /** Loaded in full from XML file */
423 public Area fetchArea(long area_list_id, long layer_id) {
424 return null;
427 /* Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImagePlust.getProcessor().
428 * or just use the Patch.getImageProcessor() method which does it for you. */
429 public ImagePlus fetchImagePlus(final Patch p) {
430 return (ImagePlus)fetchImage(p, Layer.IMAGEPLUS);
433 /** Fetch the ImageProcessor in a synchronized manner, so that there are no conflicts in retrieving the ImageProcessor for a specific stack slice, for example.
434 * Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImageProcessor,
435 * or just use the Patch.getImageProcessor() method which does it for you. */
436 public ImageProcessor fetchImageProcessor(final Patch p) {
437 return (ImageProcessor)fetchImage(p, Layer.IMAGEPROCESSOR);
440 /** So far accepts Layer.IMAGEPLUS and Layer.IMAGEPROCESSOR as format. */
441 public Object fetchImage(final Patch p, final int format) {
442 ImagePlus imp = null;
443 ImageProcessor ip = null;
444 String slice = null;
445 String path = null;
446 long n_bytes = 0;
447 PatchLoadingLock plock = null;
448 synchronized (db_lock) {
449 lock();
450 imp = imps.get(p.getId());
451 try {
452 path = getAbsolutePath(p);
453 int i_sl = -1;
454 if (null != path) i_sl = path.lastIndexOf("-----#slice=");
455 if (-1 != i_sl) {
456 // activate proper slice
457 if (null != imp) {
458 // check that the stack is large enough (user may have changed it)
459 final int ia = Integer.parseInt(path.substring(i_sl + 12));
460 if (ia <= imp.getNSlices()) {
461 if (null == imp.getStack() || null == imp.getStack().getPixels(ia)) {
462 // reload (happens when closing a stack that was opened before importing it, and then trying to paint, for example)
463 imps.remove(p.getId());
464 imp = null;
465 } else {
466 imp.setSlice(ia);
467 switch (format) {
468 case Layer.IMAGEPROCESSOR:
469 ip = imp.getStack().getProcessor(ia);
470 unlock();
471 return ip;
472 case Layer.IMAGEPLUS:
473 unlock();
474 return imp;
475 default:
476 Utils.log("FSLoader.fetchImage: Unknown format " + format);
477 return null;
480 } else {
481 unlock();
482 return null; // beyond bonds!
486 // for non-stack images
487 if (null != imp) {
488 unlock();
489 switch (format) {
490 case Layer.IMAGEPROCESSOR:
491 return imp.getProcessor();
492 case Layer.IMAGEPLUS:
493 return imp;
494 default:
495 Utils.log("FSLoader.fetchImage: Unknown format " + format);
496 return null;
499 if (-1 != i_sl) {
500 slice = path.substring(i_sl);
501 // set path proper
502 path = path.substring(0, i_sl);
505 releaseMemory(); // ensure there is a minimum % of free memory
506 plock = getOrMakePatchLoadingLock(p, 0);
507 } catch (Exception e) {
508 IJError.print(e);
509 return null;
510 } finally {
511 unlock();
516 synchronized (plock) {
517 plock.lock();
519 imp = imps.get(p.getId());
520 if (null != imp) {
521 // was loaded by a different thread -- TODO the slice of the stack could be wrong!
522 plock.unlock();
523 switch (format) {
524 case Layer.IMAGEPROCESSOR:
525 return imp.getProcessor();
526 case Layer.IMAGEPLUS:
527 return imp;
528 default:
529 Utils.log("FSLoader.fetchImage: Unknown format " + format);
530 return null;
534 // going to load:
537 // reserve memory:
538 synchronized (db_lock) {
539 lock();
540 n_bytes = estimateImageFileSize(p, 0);
541 max_memory -= n_bytes;
542 unlock();
545 releaseToFit(n_bytes);
546 imp = openImage(path);
548 preProcess(imp);
550 synchronized (db_lock) {
551 try {
552 lock();
553 max_memory += n_bytes;
555 if (null == imp) {
556 if (!hs_unloadable.contains(p)) {
557 Utils.log("FSLoader.fetchImagePlus: no image exists for patch " + p + " at path " + path);
558 hs_unloadable.add(p);
560 if (ControlWindow.isGUIEnabled()) {
561 FilePathRepair.add(p);
563 removePatchLoadingLock(plock);
564 unlock();
565 plock.unlock();
566 return null;
568 // update all clients of the stack, if any
569 if (null != slice) {
570 String rel_path = getPath(p); // possibly relative
571 final int r_isl = rel_path.lastIndexOf("-----#slice");
572 if (-1 != r_isl) rel_path = rel_path.substring(0, r_isl); // should always happen
573 for (Iterator<Map.Entry<Long,String>> it = ht_paths.entrySet().iterator(); it.hasNext(); ) {
574 final Map.Entry<Long,String> entry = it.next();
575 final String str = entry.getValue(); // this is like calling getPath(p)
576 //Utils.log2("processing " + str);
577 if (0 != str.indexOf(rel_path)) {
578 //Utils.log2("SKIP str is: " + str + "\t but path is: " + rel_path);
579 continue; // get only those whose path is identical, of course!
581 final int isl = str.lastIndexOf("-----#slice=");
582 if (-1 != isl) {
583 //int i_slice = Integer.parseInt(str.substring(isl + 12));
584 final long lid = entry.getKey();
585 imps.put(lid, imp);
588 // set proper active slice
589 final int ia = Integer.parseInt(slice.substring(12));
590 imp.setSlice(ia);
591 if (Layer.IMAGEPROCESSOR == format) ip = imp.getStack().getProcessor(ia); // otherwise creates one new for nothing
592 } else {
593 // for non-stack images
594 // OBSOLETE and wrong //p.putMinAndMax(imp); // non-destructive contrast: min and max -- WRONG, it's destructive for ColorProcessor and ByteProcessor!
595 // puts the Patch min and max values into the ImagePlus processor.
596 imps.put(p.getId(), imp);
597 if (Layer.IMAGEPROCESSOR == format) ip = imp.getProcessor();
599 // imp is cached, so:
600 removePatchLoadingLock(plock);
602 } catch (Exception e) {
603 IJError.print(e);
605 unlock();
606 plock.unlock();
607 switch (format) {
608 case Layer.IMAGEPROCESSOR:
609 return ip; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
610 case Layer.IMAGEPLUS:
611 return imp;
612 default:
613 Utils.log("FSLoader.fetchImage: Unknown format " + format);
614 return null;
621 /** Returns the alpha mask image from a file, or null if none stored. */
622 @Override
623 public ByteProcessor fetchImageMask(final Patch p) {
624 // Else, see if there is a file for the Patch:
625 final String path = getAlphaPath(p);
626 if (null == path) return null;
627 // Open the mask image, which should be a compressed float tif.
628 final ImagePlus imp = opener.openImage(path);
629 if (null == imp) {
630 //Utils.log2("No mask found or could not open mask image for patch " + p + " from " + path);
631 return null;
633 final ByteProcessor mask = (ByteProcessor)imp.getProcessor().convertToByte(false);
634 //Utils.log2("Mask dimensions: " + mask.getWidth() + " x " + mask.getHeight() + " for patch " + p);
635 if (mask.getWidth() != p.getOWidth() || mask.getHeight() != p.getOHeight()) {
636 Utils.log2("Mask has improper dimensions: " + mask.getWidth() + " x " + mask.getHeight() + " for patch " + p + " which is of " + p.getOWidth() + " x " + p.getOHeight());
637 return null;
639 return mask;
642 @Override
643 public String getAlphaPath(final Patch p) {
644 final String filename = getInternalFileName(p);
645 if (null == filename) {
646 Utils.log2("null filepath!");
647 return null;
649 final String dir = getMasksFolder();
650 return new StringBuffer(dir).append(createIdPath(Long.toString(p.getId()), filename, ".zip")).toString();
653 @Override
654 public void storeAlphaMask(final Patch p, final ByteProcessor fp) {
655 // would fail if user deletes the trakem2.masks/ folder from the storage folder after having set dir_masks. But that is his problem.
656 final String path = getAlphaPath(p);
657 File parent = new File(path).getParentFile();
658 parent.mkdirs();
659 IJ.redirectErrorMessages();
660 new FileSaver(new ImagePlus("mask", fp)).saveAsZip(getAlphaPath(p));
663 public final String getMasksFolder() {
664 if (null == dir_masks) createMasksFolder();
665 return dir_masks;
668 synchronized private final void createMasksFolder() {
669 if (null == dir_masks) dir_masks = getUNUIdFolder() + "trakem2.masks/";
670 final File f = new File(dir_masks);
671 if (f.exists() && f.isDirectory()) return;
672 try {
673 f.mkdirs();
674 } catch (Exception e) {
675 IJError.print(e);
679 /** Remove the file containing the given Patch's alpha mask. */
680 public final boolean removeAlphaMask(final Patch p) {
681 try {
682 File f = new File(getAlphaPath(p));
683 if (f.exists()) {
684 return f.delete();
686 return true;
687 } catch (Exception e) {
688 IJError.print(e);
690 return false;
693 /** Loaded in full from XML file */
694 public Object[] fetchLabel(DLabel label) {
695 return null;
698 /** Loads and returns the original image, which is not cached, or returns null if it's not different than the working image. */
699 synchronized public ImagePlus fetchOriginal(final Patch patch) {
700 String original_path = patch.getOriginalPath();
701 if (null == original_path) return null;
702 // else, reserve memory and open it:
703 long n_bytes = estimateImageFileSize(patch, 0);
704 // reserve memory:
705 synchronized (db_lock) {
706 lock();
707 max_memory -= n_bytes;
708 unlock();
710 try {
711 return openImage(original_path);
712 } catch (Throwable t) {
713 IJError.print(t);
714 } finally {
715 synchronized (db_lock) {
716 lock();
717 max_memory += n_bytes;
718 unlock();
721 return null;
724 public void prepare(Layer layer) {
725 //Utils.log2("FSLoader.prepare(Layer): not implemented.");
726 super.prepare(layer);
729 /* GENERIC, from DBObject calls. Records the id of the object in the HashMap ht_dbo.
730 * Always returns true. Does not check if another object has the same id.
732 public boolean addToDatabase(final DBObject ob) {
733 synchronized (db_lock) {
734 lock();
735 setChanged(true);
736 final long id = ob.getId();
737 if (id > max_id) {
738 max_id = id;
740 unlock();
742 return true;
745 public boolean updateInDatabase(final DBObject ob, final String key) {
746 // Should only be GUI-driven
747 setChanged(true);
749 if (ob.getClass() == Patch.class) {
750 Patch p = (Patch)ob;
751 if (key.equals("tiff_working")) return null != setImageFile(p, fetchImagePlus(p));
753 return true;
756 public boolean removeFromDatabase(final DBObject ob) {
757 synchronized (db_lock) {
758 lock();
759 setChanged(true);
760 // remove from the hashtable
761 final long loid = ob.getId();
762 Utils.log2("removing " + Project.getName(ob.getClass()) + " " + ob);
763 if (ob.getClass() == Patch.class) {
764 // STRATEGY change: images are not owned by the FSLoader.
765 Patch p = (Patch)ob;
766 if (!ob.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p);
767 ht_paths.remove(p.getId()); // after removeMipMaps !
768 mawts.removeAndFlush(loid);
769 final ImagePlus imp = imps.remove(loid);
770 if (null != imp) {
771 if (imp.getStackSize() > 1) {
772 if (null == imp.getProcessor()) {}
773 else if (null == imp.getProcessor().getPixels()) {}
774 else Loader.flush(imp); // only once
775 } else {
776 Loader.flush(imp);
779 cannot_regenerate.remove(p);
780 unlock();
781 flushMipMaps(p.getId()); // locks on its own
782 touched_mipmaps.remove(p);
783 return true;
785 unlock();
787 return true;
790 /** Returns the absolute path to a file that contains the given ImagePlus image - which may be the path as described in the ImagePlus FileInfo object itself, or a totally new file.
791 * If the Patch p current image path is different than its original image path, then the file is overwritten if it exists already.
793 public String setImageFile(final Patch p, final ImagePlus imp) {
794 if (null == imp) return null;
795 try {
796 String path = getAbsolutePath(p);
797 String slice = null;
799 // path can be null if the image is pasted, or from a copy, or totally new
800 if (null != path) {
801 int i_sl = path.lastIndexOf("-----#slice=");
802 if (-1 != i_sl) {
803 slice = path.substring(i_sl);
804 path = path.substring(0, i_sl);
806 } else {
807 // no path, inspect image FileInfo's path if the image has no changes
808 if (!imp.changes) {
809 final FileInfo fi = imp.getOriginalFileInfo();
810 if (null != fi && null != fi.directory && null != fi.fileName) {
811 final String fipath = fi.directory.replace('\\', '/') + "/" + fi.fileName;
812 if (new File(fipath).exists()) {
813 // no need to save a new image, it exists and has no changes
814 updatePaths(p, fipath, null != slice);
815 cacheAll(p, imp);
816 Utils.log2("Reusing image file: path exists for fileinfo at " + fipath);
817 return fipath;
822 if (null != path) {
823 final String starting_path = path;
824 // Save as a separate image in a new path within the storage folder
826 String filename = path.substring(path.lastIndexOf('/') +1);
828 //Utils.log2("filename 1: " + filename);
830 // remove .tif extension if there
831 if (filename.endsWith(".tif")) filename = filename.substring(0, filename.length() -3); // keep the dot
833 //Utils.log2("filename 2: " + filename);
835 // check if file ends with a tag of form ".id1234." where 1234 is p.getId()
836 final String tag = ".id" + p.getId() + ".";
837 if (!filename.endsWith(tag)) filename += tag.substring(1); // without the starting dot, since it has one already
838 // reappend extension
839 filename += "tif";
841 //Utils.log2("filename 3: " + filename);
843 path = getImageStorageFolder() + filename;
845 if (path.equals(p.getOriginalPath())) {
846 // Houston, we have a problem: a user reused a non-original image
847 File file = null;
848 int i = 1;
849 final int itag = path.lastIndexOf(tag);
850 do {
851 path = path.substring(0, itag) + "." + i + tag + "tif";
852 i++;
853 file = new File(path);
854 } while (file.exists());
857 //Utils.log2("path to use: " + path);
859 final String path2 = super.exportImage(p, imp, path, true);
861 //Utils.log2("path exported to: " + path2);
863 // update paths' hashtable
864 if (null != path2) {
865 updatePaths(p, path2, null != slice);
866 cacheAll(p, imp);
867 hs_unloadable.remove(p);
868 return path2;
869 } else {
870 Utils.log("WARNING could not save image at " + path);
871 // undo
872 updatePaths(p, starting_path, null != slice);
873 return null;
876 } catch (Exception e) {
877 IJError.print(e);
879 return null;
883 * TODO
884 * Never used. Was this planned to be what we do no with DBObject.getUniqueId()?
886 private final String makeFileTitle(final Patch p) {
887 String title = p.getTitle();
888 if (null == title) return "image-" + p.getId();
889 title = asSafePath(title);
890 if (0 == title.length()) return "image-" + p.getId();
891 return title;
894 /** Associate patch with imp, and all slices as well if any. */
895 private void cacheAll(final Patch p, final ImagePlus imp) {
896 if (p.isStack()) {
897 for (Patch pa : p.getStackPatches()) {
898 cache(pa, imp);
900 } else {
901 cache(p, imp);
905 /** For the Patch and for any associated slices if the patch is part of a stack. */
906 private void updatePaths(final Patch patch, final String path, final boolean is_stack) {
907 synchronized (db_lock) {
908 lock();
909 try {
910 // ensure the old path is cached in the Patch, to get set as the original if there is no original.
911 if (is_stack) {
912 for (Patch p : patch.getStackPatches()) {
913 long pid = p.getId();
914 String str = ht_paths.get(pid);
915 int isl = str.lastIndexOf("-----#slice=");
916 updatePatchPath(p, path + str.substring(isl));
918 } else {
919 Utils.log2("path to set: " + path);
920 Utils.log2("path before: " + ht_paths.get(patch.getId()));
921 updatePatchPath(patch, path);
922 Utils.log2("path after: " + ht_paths.get(patch.getId()));
924 } catch (Throwable e) {
925 IJError.print(e);
926 } finally {
927 unlock();
932 /** With slice info appended at the end; only if it exists, otherwise null. */
933 public String getAbsolutePath(final Patch patch) {
934 String abs_path = patch.getCurrentPath();
935 if (null != abs_path) return abs_path;
936 // else, compute, set and return it:
937 String path = ht_paths.get(patch.getId());
938 if (null == path) return null;
939 // substract slice info if there
940 int i_sl = path.lastIndexOf("-----#slice=");
941 String slice = null;
942 if (-1 != i_sl) {
943 slice = path.substring(i_sl);
944 path = path.substring(0, i_sl);
946 if (isRelativePath(path)) {
947 // path is relative: preprend the parent folder of the xml file
948 path = getParentFolder() + path;
949 if (!isURL(path) && !new File(path).exists()) {
950 Utils.log("Path for patch " + patch + " does not exist: " + path);
951 return null;
953 // else assume that it exists
955 // reappend slice info if existent
956 if (null != slice) path += slice;
957 // set it
958 patch.cacheCurrentPath(path);
959 return path;
962 public final String getAbsoluteFilePath(final Patch p) {
963 final String path = getAbsolutePath(p);
964 if (null == path) return null;
965 final int i = path.lastIndexOf("----#slice");
966 return -1 == i ? path
967 : path.substring(0, i);
970 public static final boolean isURL(final String path) {
971 return null != path && 0 == path.indexOf("http://");
974 static public final Pattern ABS_PATH = Pattern.compile("^[a-zA-Z]*:/.*$|^/.*$|[a-zA-Z]:.*$");
976 public static final boolean isRelativePath(final String path) {
977 return ! ABS_PATH.matcher(path).matches();
980 /** All backslashes are converted to slashes to avoid havoc in MSWindows. */
981 public void addedPatchFrom(String path, final Patch patch) {
982 if (null == path) {
983 Utils.log("Null path for patch: " + patch);
984 return;
986 updatePatchPath(patch, path);
989 /** This method has the exclusivity in calling ht_paths.put, because it ensures the path won't have escape characters. */
990 private final void updatePatchPath(final Patch patch, String path) { // reversed order in purpose, relative to addedPatchFrom
991 // avoid W1nd0ws nightmares
992 path = path.replace('\\', '/'); // replacing with chars, in place
993 // remove double slashes that a user may have slipped in
994 final int start = isURL(path) ? 6 : (IJ.isWindows() ? 3 : 1);
995 while (-1 != path.indexOf("//", start)) {
996 // avoid the potential C:// of windows and the starting // of a samba network
997 path = path.substring(0, start) + path.substring(start).replace("//", "/");
999 // cache path as absolute
1000 patch.cacheCurrentPath(isRelativePath(path) ? getParentFolder() + path : path);
1001 // if path is absolute, try to make it relative
1002 //Utils.log2("path was: " + path);
1003 path = makeRelativePath(path);
1004 // store
1005 ht_paths.put(patch.getId(), path);
1006 //Utils.log2("Updated patch path " + ht_paths.get(patch.getId()) + " for patch " + patch);
1009 /** Takes a String and returns a copy with the following conversions: / to -, space to _, and \ to -. */
1010 public String asSafePath(final String name) {
1011 return name.trim().replace('/', '-').replace(' ', '_').replace('\\','-');
1014 /** Overwrites the XML file. If some images do not exist in the file system, a directory with the same name of the XML file plus an "_images" tag appended will be created and images saved there. */
1015 public String save(final Project project) {
1016 String result = null;
1017 if (null == project_file_path) {
1018 String xml_path = super.saveAs(project, null, false);
1019 if (null == xml_path) return null;
1020 else {
1021 this.project_file_path = xml_path;
1022 ControlWindow.updateTitle(project);
1023 result = this.project_file_path;
1025 } else {
1026 File fxml = new File(project_file_path);
1027 result = super.export(project, fxml, false);
1029 if (null != result) {
1030 Utils.logAll(Utils.now() + " Saved " + project);
1031 touched_mipmaps.clear();
1033 return result;
1036 public String saveAs(Project project) {
1037 String path = super.saveAs(project, null, false);
1038 if (null != path) {
1039 // update the xml path to point to the new one
1040 this.project_file_path = path;
1041 Utils.log2("After saveAs, new xml path is: " + path);
1043 ControlWindow.updateTitle(project);
1044 return path;
1047 /** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
1048 public String saveAs(final String path, final boolean overwrite) {
1049 if (null == path) {
1050 Utils.log("Cannot save on null path.");
1051 return null;
1053 String path2 = path;
1054 if (!path2.endsWith(".xml")) path2 += ".xml";
1055 File fxml = new File(path2);
1056 if (!fxml.canWrite()) {
1057 // write to storage folder instead
1058 String path3 = path2;
1059 path2 = getStorageFolder() + fxml.getName();
1060 Utils.logAll("WARNING can't write to " + path3 + "\n --> will write instead to " + path2);
1061 fxml = new File(path2);
1063 if (!overwrite) {
1064 int i = 1;
1065 while (fxml.exists()) {
1066 String parent = fxml.getParent().replace('\\','/');
1067 if (!parent.endsWith("/")) parent += "/";
1068 String name = fxml.getName();
1069 name = name.substring(0, name.length() - 4);
1070 path2 = parent + name + "-" + i + ".xml";
1071 fxml = new File(path2);
1072 i++;
1075 Project project = Project.findProject(this);
1076 path2 = super.saveAs(project, path2, false);
1077 if (null != path2) {
1078 project_file_path = path2;
1079 Utils.logAll("After saveAs, new xml path is: " + path2);
1080 ControlWindow.updateTitle(project);
1081 touched_mipmaps.clear();
1083 return path2;
1086 /** Returns the stored path for the given Patch image, which may be relative and may contain slice information appended.*/
1087 public String getPath(final Patch patch) {
1088 return ht_paths.get(patch.getId());
1091 /** Takes the given path and tries to makes it relative to this instance's project_file_path, if possible. Otherwise returns the argument as is. */
1092 private String makeRelativePath(String path) {
1093 if (null == project_file_path) {
1094 //unsaved project
1095 return path;
1097 if (null == path) {
1098 return null;
1100 // fix W1nd0ws paths
1101 path = path.replace('\\', '/'); // char-based, no parsing problems
1102 // remove slice tag
1103 String slice = null;
1104 int isl = path.lastIndexOf("-----#slice");
1105 if (-1 != isl) {
1106 slice = path.substring(isl);
1107 path = path.substring(0, isl);
1110 if (isRelativePath(path)) {
1111 // already relative
1112 if (-1 != isl) path += slice;
1113 return path;
1115 // the long and verbose way, to be cross-platform. Should work with URLs just the same.
1116 String xdir = new File(project_file_path).getParentFile().getAbsolutePath();
1117 if (!xdir.endsWith("/")) xdir += "/";
1118 if (IJ.isWindows()) {
1119 xdir = xdir.replace('\\', '/');
1120 path = path.replace('\\', '/');
1122 if (path.startsWith(xdir)) {
1123 path = path.substring(xdir.length());
1125 if (-1 != isl) path += slice;
1126 //Utils.log("made relative path: " + path);
1127 return path;
1130 /** Adds a "Save" and "Save as" menu items. */
1131 public void setupMenuItems(final JMenu menu, final Project project) {
1132 ActionListener listener = new ActionListener() {
1133 public void actionPerformed(ActionEvent ae) {
1134 String command = ae.getActionCommand();
1135 if (command.equals("Save")) {
1136 save(project);
1137 } else if (command.equals("Save as...")) {
1138 saveAs(project);
1142 JMenuItem item;
1143 item = new JMenuItem("Save"); item.addActionListener(listener); menu.add(item);
1144 item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true));
1145 item = new JMenuItem("Save as..."); item.addActionListener(listener); menu.add(item);
1148 /** Returns the last Patch. */
1149 protected Patch importStackAsPatches(final Project project, final Layer first_layer, final int x, final int y, final ImagePlus imp_stack, final boolean as_copy, final String filepath) {
1150 Utils.log2("FSLoader.importStackAsPatches filepath=" + filepath);
1151 String target_dir = null;
1152 if (as_copy) {
1153 DirectoryChooser dc = new DirectoryChooser("Folder to save images");
1154 target_dir = dc.getDirectory();
1155 if (null == target_dir) return null; // user canceled dialog
1156 if (target_dir.length() -1 != target_dir.lastIndexOf('/')) {
1157 target_dir += "/";
1161 final boolean virtual = imp_stack.getStack().isVirtual();
1163 int pos_x = Integer.MAX_VALUE != x ? x : (int)first_layer.getLayerWidth()/2 - imp_stack.getWidth()/2;
1164 int pos_y = Integer.MAX_VALUE != y ? y : (int)first_layer.getLayerHeight()/2 - imp_stack.getHeight()/2;
1165 final double thickness = first_layer.getThickness();
1166 final String title = Utils.removeExtension(imp_stack.getTitle()).replace(' ', '_');
1167 Utils.showProgress(0);
1168 Patch previous_patch = null;
1169 final int n = imp_stack.getStackSize();
1170 for (int i=1; i<=n; i++) {
1171 Layer layer = first_layer;
1172 double z = first_layer.getZ() + (i-1) * thickness;
1173 if (i > 1) layer = first_layer.getParent().getLayer(z, thickness, true); // will create new layer if not found
1174 if (null == layer) {
1175 Utils.log("Display.importStack: could not create new layers.");
1176 return null;
1178 String patch_path = null;
1180 ImagePlus imp_patch_i = null;
1181 if (virtual) { // because we love inefficiency, every time all this is done again
1182 VirtualStack vs = (VirtualStack)imp_stack.getStack();
1183 String vs_dir = vs.getDirectory().replace('\\', '/');
1184 if (!vs_dir.endsWith("/")) vs_dir += "/";
1185 String iname = vs.getFileName(i);
1186 patch_path = vs_dir + iname;
1187 Utils.log2("virtual stack: patch path is " + patch_path);
1188 releaseMemory();
1189 Utils.log2(i + " : " + patch_path);
1190 imp_patch_i = openImage(patch_path);
1191 } else {
1192 ImageProcessor ip = imp_stack.getStack().getProcessor(i);
1193 if (as_copy) ip = ip.duplicate();
1194 imp_patch_i = new ImagePlus(title + "__slice=" + i, ip);
1196 preProcess(imp_patch_i);
1198 String label = imp_stack.getStack().getSliceLabel(i);
1199 if (null == label) label = "";
1200 Patch patch = null;
1201 if (as_copy) {
1202 patch_path = target_dir + imp_patch_i.getTitle() + ".zip";
1203 ini.trakem2.io.ImageSaver.saveAsZip(imp_patch_i, patch_path);
1204 patch = new Patch(project, label + " " + title + " " + i, pos_x, pos_y, imp_patch_i);
1205 } else if (virtual) {
1206 patch = new Patch(project, label, pos_x, pos_y, imp_patch_i);
1207 } else {
1208 patch_path = filepath + "-----#slice=" + i;
1209 //Utils.log2("path is "+ patch_path);
1210 final AffineTransform atp = new AffineTransform();
1211 atp.translate(pos_x, pos_y);
1212 patch = new Patch(project, getNextId(), label + " " + title + " " + i, imp_stack.getWidth(), imp_stack.getHeight(), imp_stack.getType(), false, imp_stack.getProcessor().getMin(), imp_stack.getProcessor().getMax(), atp);
1213 patch.addToDatabase();
1214 //Utils.log2("type is " + imp_stack.getType());
1216 Utils.log2("B: " + i + " : " + patch_path);
1217 addedPatchFrom(patch_path, patch);
1218 if (!as_copy && !virtual) {
1219 if (virtual) cache(patch, imp_patch_i); // each slice separately
1220 else cache(patch, imp_stack); // uses the entire stack, shared among all Patch instances
1222 if (isMipMapsEnabled()) generateMipMaps(patch);
1223 if (null != previous_patch) patch.link(previous_patch);
1224 layer.add(patch);
1225 previous_patch = patch;
1226 Utils.showProgress(i * (1.0 / n));
1228 Utils.showProgress(1.0);
1230 // update calibration
1231 // TODO
1233 // return the last patch
1234 return previous_patch;
1237 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1238 public void parseXMLOptions(final HashMap ht_attributes) {
1239 Object ob = ht_attributes.remove("preprocessor");
1240 if (null != ob) {
1241 setPreprocessor((String)ob);
1243 // Adding some logic to support old projects which lack a storage folder and a mipmaps folder
1244 // and also to prevent errors such as those created when manualy tinkering with the XML file
1245 // or renaming directories, etc.
1246 ob = ht_attributes.remove("storage_folder");
1247 if (null != ob) {
1248 String sf = ((String)ob).replace('\\', '/');
1249 if (isRelativePath(sf)) {
1250 sf = getParentFolder() + sf;
1252 if (isURL(sf)) {
1253 // can't be an URL
1254 Utils.log2("Can't have an URL as the path of a storage folder.");
1255 } else {
1256 File f = new File(sf);
1257 if (f.exists() && f.isDirectory()) {
1258 this.dir_storage = sf;
1259 } else {
1260 Utils.log2("storage_folder was not found or is invalid: " + ob);
1264 if (null == this.dir_storage) {
1265 // select the directory where the xml file lives.
1266 this.dir_storage = getParentFolder();
1267 if (null == this.dir_storage || isURL(this.dir_storage)) this.dir_storage = null;
1268 if (null == this.dir_storage && ControlWindow.isGUIEnabled()) {
1269 Utils.log2("Asking user for a storage folder in a dialog."); // tip for headless runners whose program gets "stuck"
1270 DirectoryChooser dc = new DirectoryChooser("REQUIRED: select a storage folder");
1271 this.dir_storage = dc.getDirectory();
1273 if (null == this.dir_storage) {
1274 IJ.showMessage("TrakEM2 requires a storage folder.\nTemporarily your home directory will be used.");
1275 this.dir_storage = System.getProperty("user.home").replace('\\', '/');
1278 // fix
1279 if (null != this.dir_storage && !this.dir_storage.endsWith("/")) this.dir_storage += "/";
1280 Utils.log2("storage folder is " + this.dir_storage);
1282 ob = ht_attributes.remove("mipmaps_folder");
1283 if (null != ob) {
1284 String mf = ((String)ob).replace('\\', '/');
1285 if (isRelativePath(mf)) {
1286 mf = getParentFolder() + mf;
1288 if (isURL(mf)) {
1289 this.dir_mipmaps = mf;
1290 // TODO must disable input somehow, so that images are not edited.
1291 } else {
1292 File f = new File(mf);
1293 if (f.exists() && f.isDirectory()) {
1294 this.dir_mipmaps = mf;
1295 } else {
1296 Utils.log2("mipmaps_folder was not found or is invalid: " + ob);
1301 // parse the unuid before attempting to create any folders
1302 this.unuid = (String) ht_attributes.remove("unuid");
1304 if (null == this.dir_mipmaps) {
1305 // create a new one inside the dir_storage, which can't be null
1306 createMipMapsDir(dir_storage);
1307 if (null != this.dir_mipmaps && ControlWindow.isGUIEnabled() && null != IJ.getInstance()) {
1308 askAndExecMipmapRegeneration(null);
1311 // fix
1312 if (null != this.dir_mipmaps && !this.dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
1313 Utils.log2("mipmaps folder is " + this.dir_mipmaps);
1315 if (null == unuid) {
1316 IJ.log("OLD VERSION DETECTED: your trakem2\nproject has been updated to the new format.\nPlease SAVE IT to avoid regenerating\ncached data when reopening it.");
1317 if (ControlWindow.isGUIEnabled()) {
1318 obtainUNUIdFolder();
1319 if (null != this.unuid) return; // one was selected, which was hopefully valid
1321 Utils.log2("Creating unuid for project " + this);
1322 this.unuid = createUNUId(dir_storage);
1323 fixStorageFolders();
1324 Utils.log2("Now mipmaps folder is " + this.dir_mipmaps);
1325 if (null != dir_masks) Utils.log2("Now masks folder is " + this.dir_masks);
1329 private void askAndExecMipmapRegeneration(final String msg) {
1330 Utils.log2("Asking user Yes/No to generate mipmaps on the background."); // tip for headless runners whose program gets "stuck"
1331 YesNoDialog yn = new YesNoDialog(IJ.getInstance(), "Generate mipmaps", (null != msg ? msg + "\n" : "") + "Generate mipmaps in the background for all images?");
1332 if (yn.yesPressed()) {
1333 final Loader lo = this;
1334 new Thread() {
1335 public void run() {
1336 try {
1337 // wait while parsing the rest of the XML file
1338 while (!v_loaders.contains(lo)) {
1339 Thread.sleep(1000);
1341 Project pj = Project.findProject(lo);
1342 // Submit a task for each Patch:
1343 for (final Displayable patch : pj.getRootLayerSet().getDisplayables(Patch.class)) {
1344 ((FSLoader)lo).regenerateMipMaps((Patch)patch);
1346 } catch (Exception e) {}
1348 }.start();
1352 /** Specific options for the Loader which exist as attributes to the Project XML node. */
1353 public void insertXMLOptions(StringBuffer sb_body, String indent) {
1354 sb_body.append(indent).append("unuid=\"").append(unuid).append("\"\n");
1355 if (null != preprocessor) sb_body.append(indent).append("preprocessor=\"").append(preprocessor).append("\"\n");
1356 if (null != dir_mipmaps) sb_body.append(indent).append("mipmaps_folder=\"").append(makeRelativePath(dir_mipmaps)).append("\"\n");
1357 if (null != dir_storage) sb_body.append(indent).append("storage_folder=\"").append(makeRelativePath(dir_storage)).append("\"\n");
1360 /** Return the path to the folder containing the project XML file. */
1361 private final String getParentFolder() {
1362 return this.project_file_path.substring(0, this.project_file_path.lastIndexOf('/')+1);
1365 /* ************** MIPMAPS **********************/
1367 /** Returns the path to the directory hosting the file image pyramids. */
1368 public String getMipMapsFolder() {
1369 return dir_mipmaps;
1374 static private IndexColorModel thresh_cm = null;
1376 static private final IndexColorModel getThresholdLUT() {
1377 if (null == thresh_cm) {
1378 // An array of all black pixels (value 0) except at 255, which is white (value 255).
1379 final byte[] c = new byte[256];
1380 c[255] = (byte)255;
1381 thresh_cm = new IndexColorModel(8, 256, c, c, c);
1383 return thresh_cm;
1387 /** Returns the array of pixels, whose type depends on the bi.getType(); for example, for a BufferedImage.TYPE_BYTE_INDEXED, returns a byte[]. */
1388 static public final Object grabPixels(final BufferedImage bi) {
1389 final PixelGrabber pg = new PixelGrabber(bi, 0, 0, bi.getWidth(), bi.getHeight(), false);
1390 try {
1391 pg.grabPixels();
1392 return pg.getPixels();
1393 } catch (InterruptedException e) {
1394 IJError.print(e);
1396 return null;
1399 private final BufferedImage createCroppedAlpha(final BufferedImage alpha, final BufferedImage outside) {
1400 if (null == outside) return alpha;
1402 final int width = outside.getWidth();
1403 final int height = outside.getHeight();
1405 // Create an outside image, thresholded: only pixels of 255 remain as 255, the rest is set to 0.
1406 /* // DOESN'T work: creates a mask with "black" as 254 (???), and white 255 (correct).
1407 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, getThresholdLUT());
1408 thresholded.createGraphics().drawImage(outside, 0, 0, null);
1411 // So, instead: grab the pixels, fix them manually
1412 // The cast to byte[] works because "outside" and "alpha" are TYPE_BYTE_INDEXED.
1413 final byte[] o = (byte[])grabPixels(outside);
1414 if (null == o) return null;
1415 final byte[] a = null == alpha ? o : (byte[])grabPixels(alpha);
1417 // Set each non-255 pixel in outside to 0 in alpha:
1418 for (int i=0; i<o.length; i++) {
1419 if ( (o[i]&0xff) < 255) a[i] = 0;
1422 // Put the pixels back into an image:
1423 final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
1424 thresholded.getRaster().setDataElements(0, 0, width, height, a);
1426 return thresholded;
1429 static public final BufferedImage convertToBufferedImage(final ByteProcessor bp) {
1430 bp.setMinAndMax(0, 255);
1431 final Image img = bp.createImage();
1432 if (img instanceof BufferedImage) return (BufferedImage)img;
1433 //else:
1434 final BufferedImage bi = new BufferedImage(bp.getWidth(), bp.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
1435 bi.createGraphics().drawImage(bi, 0, 0, null);
1436 return bi;
1439 /** Scale a BufferedImage.TYPE_BYTE_INDEXED into another of the same type but dimensions target_width,target_height. */
1440 static private final BufferedImage scaleAndFlush(final Image img, final int target_width, final int target_height, final boolean area_averaging, final Object interpolation_hint) {
1441 final BufferedImage bi = new BufferedImage(target_width, target_height, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
1442 if (area_averaging) {
1443 bi.createGraphics().drawImage(img.getScaledInstance(target_width, target_height, Image.SCALE_AREA_AVERAGING), 0, 0, null);
1444 } else {
1445 final Graphics2D g = bi.createGraphics();
1446 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation_hint);
1447 g.drawImage(img, 0, 0, target_width, target_height, null); // draws it scaled to target area w*h
1449 // Release native resources
1450 img.flush();
1452 return bi;
1455 /** Image to BufferedImage. Can be used for hardware-accelerated resizing, since the whole awt is painted to a target w,h area in the returned new BufferedImage. Does not accept LUT images: only ARGB or GRAY. */
1456 private final BufferedImage[] IToBI(final Image awt, final int w, final int h, final Object interpolation_hint, final BufferedImage alpha, final BufferedImage outside) {
1457 BufferedImage bi;
1458 final boolean area_averaging = interpolation_hint.getClass() == Integer.class && Loader.AREA_AVERAGING == ((Integer)interpolation_hint).intValue();
1459 final boolean must_scale = (w != awt.getWidth(null) || h != awt.getHeight(null));
1461 if (null != alpha || null != outside) bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1462 else bi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
1463 final Graphics2D g = bi.createGraphics();
1464 if (area_averaging) {
1465 final Image img = awt.getScaledInstance(w, h, Image.SCALE_AREA_AVERAGING); // Creates ALWAYS an RGB image, so must repaint back to a single-channel image, avoiding unnecessary blow up of memory.
1466 g.drawImage(img, 0, 0, null);
1467 } else {
1468 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation_hint);
1469 g.drawImage(awt, 0, 0, w, h, null); // draws it scaled
1471 BufferedImage ba = alpha;
1472 BufferedImage bo = outside;
1473 if (null != alpha && must_scale) {
1474 ba = scaleAndFlush(alpha, w, h, area_averaging, interpolation_hint);
1476 if (null != outside && must_scale) {
1477 bo = scaleAndFlush(outside, w, h, area_averaging, interpolation_hint);
1480 BufferedImage the_alpha = ba;
1481 if (null != alpha) {
1482 if (null != outside) {
1483 the_alpha = createCroppedAlpha(ba, bo);
1485 } else if (null != outside) {
1486 the_alpha = createCroppedAlpha(null, bo);
1488 if (null != the_alpha) {
1489 bi.getAlphaRaster().setRect(the_alpha.getRaster());
1490 //bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])new ImagePlus("", the_alpha).getProcessor().convertToFloat().getPixels());
1491 the_alpha.flush();
1494 //Utils.log2("bi is: " + bi.getType() + " BufferedImage.TYPE_INT_ARGB=" + BufferedImage.TYPE_INT_ARGB);
1497 FloatProcessor fp_alpha = null;
1498 fp_alpha = (FloatProcessor) new ByteProcessor(ba).convertToFloat();
1499 // Set all non-white pixels to zero (eliminate shadowy border caused by interpolation)
1500 final float[] pix = (float[])fp_alpha.getPixels();
1501 for (int i=0; i<pix.length; i++)
1502 if (Math.abs(pix[i] - 255) > 0.001f) pix[i] = 0;
1503 bi.getAlphaRaster().setPixels(0, 0, w, h, (float[])fp_alpha.getPixels());
1506 return new BufferedImage[]{bi, ba, bo};
1509 private final Object getHint(final int mode) {
1510 switch (mode) {
1511 case Loader.BICUBIC:
1512 return RenderingHints.VALUE_INTERPOLATION_BICUBIC;
1513 case Loader.BILINEAR:
1514 return RenderingHints.VALUE_INTERPOLATION_BILINEAR;
1515 case Loader.AREA_AVERAGING:
1516 return new Integer(mode);
1517 case Loader.NEAREST_NEIGHBOR:
1518 default:
1519 return RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
1523 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1524 static final private byte[] gaussianBlurResizeInHalf(final FloatProcessorT2 source, final int source_width, final int source_height, final int target_width, final int target_height) {
1525 source.setPixels(source_width, source_height, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])source.getPixels(), source_width, source_height), 0.75f).data);
1526 source.resizeInPlace(target_width, target_height);
1527 return (byte[])source.convertToByte(false).getPixels(); // no scaling
1530 /** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
1531 static final private byte[] meanResizeInHalf(final FloatProcessorT2 source, final int sourceWidth, final int sourceHeight, final int targetWidth, final int targetHeight) {
1532 final float[] sourceData = source.getFloatPixels();
1533 final float[] targetData = new float[targetWidth * targetHeight];
1534 int rs = 0;
1535 for (int r = 0; r < targetData.length; r += targetWidth) {
1536 int xs = -1;
1537 for (int x = 0; x < targetWidth; ++x)
1538 targetData[r + x] = sourceData[rs + ++xs] + sourceData[rs + ++xs];
1539 rs += sourceWidth;
1540 xs = -1;
1541 for (int x = 0; x < targetWidth; ++x) {
1542 targetData[r + x] += sourceData[rs + ++xs] + sourceData[rs + ++xs];
1543 targetData[r + x] /= 4;
1545 rs += sourceWidth;
1547 source.setPixels(targetWidth, targetHeight, targetData);
1548 return (byte[])source.convertToByte(false).getPixels();
1551 /** Queue/unqueue for mipmap removal on shutdown without saving. */
1552 public void queueForMipmapRemoval(final Patch p, boolean yes) {
1553 if (yes) touched_mipmaps.add(p);
1554 else touched_mipmaps.remove(p);
1557 /** Given an image and its source file name (without directory prepended), generate
1558 * a pyramid of images until reaching an image not smaller than 32x32 pixels.<br />
1559 * Such images are stored as jpeg 85% quality in a folder named trakem2.mipmaps.<br />
1560 * The Patch id and a ".jpg" extension will be appended to the filename in all cases.<br />
1561 * Any equally named files will be overwritten.
1563 public boolean generateMipMaps(final Patch patch) {
1564 return generateMipMaps(patch, true);
1566 /** The boolean flag is because the submission to the ExecutorService is too fast, and I need to prevent it before it can ever add duplicates to the queue, so I need to ask that question before invoking this method. */
1567 private boolean generateMipMaps(final Patch patch, final boolean check_if_already_being_done) {
1568 Utils.log2("mipmaps for " + patch);
1569 final String path = getAbsolutePath(patch);
1570 if (null == path) {
1571 Utils.log2("generateMipMaps: cannot find path for Patch " + patch);
1572 cannot_regenerate.add(patch);
1573 return false;
1575 if (hs_unloadable.contains(patch)) {
1576 FilePathRepair.add(patch);
1577 return false;
1579 synchronized (gm_lock) {
1580 try {
1581 gm_lock();
1582 if (null == dir_mipmaps) createMipMapsDir(null);
1583 if (null == dir_mipmaps || isURL(dir_mipmaps)) return false;
1584 if (check_if_already_being_done && hs_regenerating_mipmaps.contains(patch)) {
1585 // already being done
1586 Utils.log2("Already being done: " + patch);
1587 return false;
1589 hs_regenerating_mipmaps.add(patch);
1590 } catch (Exception e) {
1591 IJError.print(e);
1592 } finally {
1593 gm_unlock();
1597 /** Record Patch as modified */
1598 touched_mipmaps.add(patch);
1600 /** Remove serialized features, if any */
1601 removeSerializedFeatures(patch);
1603 /** Remove serialized pointmatches, if any */
1604 removeSerializedPointMatches(patch);
1606 String srmode = patch.getProject().getProperty("image_resizing_mode");
1607 int resizing_mode = GAUSSIAN;
1608 if (null != srmode) resizing_mode = Loader.getMode(srmode);
1610 try {
1611 // Now:
1612 // 1 - Ask the Patch to apply a coordinate transform, or rather, create a function that gets the coordinate transform from the Patch and applies it to the 'ip'.
1613 // 2 - Then (1) should return both the transformed image and the alpha mask
1615 ImageProcessor ip;
1616 ByteProcessor alpha_mask = null;
1617 ByteProcessor outside_mask = null;
1618 final boolean coordinate_transformed;
1619 int type = patch.getType();
1621 // Obtain an image which may be coordinate-transformed, and an alpha mask.
1622 Patch.PatchImage pai = patch.createTransformedImage();
1623 if (null == pai) {
1624 Utils.log("Can't regenerate mipmaps for patch " + patch);
1625 cannot_regenerate.add(patch);
1626 return false;
1628 ip = pai.target;
1629 alpha_mask = pai.mask; // can be null
1630 outside_mask = pai.outside; // can be null
1631 coordinate_transformed = pai.coordinate_transformed;
1632 pai = null;
1634 // Old style:
1635 //final String filename = new StringBuffer(new File(path).getName()).append('.').append(patch.getId()).append(".jpg").toString();
1636 // New style:
1637 final String filename = createMipMapRelPath(patch);
1639 int w = ip.getWidth();
1640 int h = ip.getHeight();
1642 // sigma = sqrt(2^level - 0.5^2)
1643 // where 0.5 is the estimated sigma for a full-scale image
1644 // which means sigma = 0.75 for the full-scale image (has level 0)
1645 // prepare a 0.75 sigma image from the original
1646 ColorModel cm = ip.getColorModel();
1647 int k = 0; // the scale level. Proper scale is: 1 / pow(2, k)
1648 // but since we scale 50% relative the previous, it's always 0.75
1650 double min = patch.getMin(),
1651 max = patch.getMax();
1652 // Fix improper min,max values
1653 // (The -1,-1 are flags really for "not set")
1654 if (-1 == min && -1 == max) {
1655 switch (type) {
1656 case ImagePlus.COLOR_RGB:
1657 case ImagePlus.COLOR_256:
1658 case ImagePlus.GRAY8:
1659 patch.setMinAndMax(0, 255);
1660 break;
1661 // Find and flow through to default:
1662 case ImagePlus.GRAY16:
1663 ((ij.process.ShortProcessor)ip).findMinAndMax();
1664 patch.setMinAndMax(ip.getMin(), ip.getMax());
1665 break;
1666 case ImagePlus.GRAY32:
1667 ((FloatProcessor)ip).findMinAndMax();
1668 patch.setMinAndMax(ip.getMin(), ip.getMax());
1669 break;
1671 min = patch.getMin(); // may have changed
1672 max = patch.getMax();
1675 // Set for the level 0 image, which is a duplicate of the one on the cache in any case
1676 ip.setMinAndMax(min, max);
1679 // Proper support for LUT images: treat them as RGB
1680 if (ip.isColorLut()) {
1681 ip = ip.convertToRGB();
1682 cm = null;
1683 type = ImagePlus.COLOR_RGB;
1686 if (ImagePlus.COLOR_RGB == type) {
1687 // TODO releaseToFit proper
1688 releaseToFit(w * h * 4 * 5);
1689 final ColorProcessor cp = (ColorProcessor)ip;
1690 final FloatProcessorT2 red = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(0, red);
1691 final FloatProcessorT2 green = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(1, green);
1692 final FloatProcessorT2 blue = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(2, blue);
1693 FloatProcessorT2 alpha;
1694 final FloatProcessorT2 outside;
1695 if (null != alpha_mask) {
1696 alpha = new FloatProcessorT2((FloatProcessor)alpha_mask.convertToFloat());
1697 } else {
1698 alpha = null;
1700 if (null != outside_mask) {
1701 outside = new FloatProcessorT2((FloatProcessor)outside_mask.convertToFloat());
1702 if ( null == alpha ) {
1703 alpha = outside;
1704 alpha_mask = outside_mask;
1706 } else {
1707 outside = null;
1710 // sw,sh are the dimensions of the image to blur
1711 // w,h are the dimensions to scale the blurred image to
1712 int sw = w,
1713 sh = h;
1715 final String target_dir0 = getLevelDir(dir_mipmaps, 0);
1716 // No alpha channel:
1717 // - use gaussian resizing
1718 // - use standard ImageJ java.awt.Image creation
1720 if (Thread.currentThread().isInterrupted()) return false;
1722 // Generate level 0 first:
1723 // TODO Add alpha information into the int[] pixel array or make the image visible some ohter way
1724 if (!(null == alpha ? ini.trakem2.io.ImageSaver.saveAsJpeg(cp, target_dir0 + filename, 0.85f, false)
1725 : ini.trakem2.io.ImageSaver.saveAsJpegAlpha(createARGBImage(w, h, embedAlpha((int[])cp.getPixels(), (byte[])alpha_mask.getPixels(), null == outside ? null : (byte[])outside_mask.getPixels())), target_dir0 + filename, 0.85f))) {
1726 cannot_regenerate.add(patch);
1727 } else {
1728 do {
1729 if (Thread.currentThread().isInterrupted()) return false;
1730 // 1 - Prepare values for the next scaled image
1731 sw = w;
1732 sh = h;
1733 w /= 2;
1734 h /= 2;
1735 k++;
1736 // 2 - Check that the target folder for the desired scale exists
1737 final String target_dir = getLevelDir(dir_mipmaps, k);
1738 if (null == target_dir) continue;
1739 // 3 - Blur the previous image to 0.75 sigma, and scale it
1740 final byte[] r = gaussianBlurResizeInHalf(red, sw, sh, w, h); // will resize 'red' FloatProcessor in place.
1741 final byte[] g = gaussianBlurResizeInHalf(green, sw, sh, w, h); // idem
1742 final byte[] b = gaussianBlurResizeInHalf(blue, sw, sh, w, h); // idem
1743 final byte[] a = null == alpha ? null : gaussianBlurResizeInHalf(alpha, sw, sh, w, h); // idem
1744 if ( null != outside ) {
1745 final byte[] o;
1746 if (alpha != outside)
1747 o = gaussianBlurResizeInHalf(outside, sw, sh, w, h); // idem
1748 else
1749 o = a;
1750 // Remove all not completely inside pixels from the alphamask
1751 // If there was no alpha mask, alpha is the outside itself
1752 for (int i=0; i<o.length; i++) {
1753 if ( (o[i]&0xff) != 255 ) a[i] = 0; // TODO I am sure there is a bitwise operation to do this in one step. Some thing like: a[i] &= 127;
1757 // 4 - Compose ColorProcessor
1758 final int[] pix = new int[w * h];
1759 if (null == alpha) {
1760 for (int i=0; i<pix.length; i++) {
1761 pix[i] = 0xff000000 | ((r[i]&0xff)<<16) | ((g[i]&0xff)<<8) | (b[i]&0xff);
1763 final ColorProcessor cp2 = new ColorProcessor(w, h, pix);
1764 // 5 - Save as jpeg
1765 if (!ini.trakem2.io.ImageSaver.saveAsJpeg(cp2, target_dir + filename, 0.85f, false)) {
1766 cannot_regenerate.add(patch);
1767 break;
1769 } else {
1770 // LIKELY no need to set alpha raster later in createARGBImage ... TODO
1771 for (int i=0; i<pix.length; i++) {
1772 pix[i] = ((a[i]&0xff)<<24) | ((r[i]&0xff)<<16) | ((g[i]&0xff)<<8) | (b[i]&0xff);
1774 final BufferedImage bi_save = createARGBImage(w, h, pix);
1775 if (!ini.trakem2.io.ImageSaver.saveAsJpegAlpha(bi_save, target_dir + filename, 0.85f)) {
1776 cannot_regenerate.add(patch);
1777 bi_save.flush();
1778 break;
1780 bi_save.flush();
1782 } while (w >= 32 && h >= 32); // not smaller than 32x32
1784 } else {
1785 // Greyscale:
1786 releaseToFit(w * h * 4 * 5);
1787 final boolean as_grey = !ip.isColorLut();
1788 if (as_grey && null == cm) {
1789 cm = GRAY_LUT;
1792 if (Thread.currentThread().isInterrupted()) return false;
1794 if (Loader.GAUSSIAN == resizing_mode) {
1795 FloatProcessor fp = (FloatProcessor) ip.convertToFloat();
1796 int sw=w, sh=h;
1798 FloatProcessor alpha;
1799 FloatProcessor outside;
1800 if (null != alpha_mask) {
1801 alpha = new FloatProcessorT2((FloatProcessor)alpha_mask.convertToFloat());
1802 } else {
1803 alpha = null;
1805 if (null != outside_mask) {
1806 outside = new FloatProcessorT2((FloatProcessor)outside_mask.convertToFloat());
1807 if (null == alpha) {
1808 alpha = outside;
1809 alpha_mask = outside_mask;
1811 } else {
1812 outside = null;
1815 do {
1817 //Utils.logAll("### k=" + k + " alpha.length=" + (null != alpha ? ((float[])alpha.getPixels()).length : 0) + " image.length=" + ((float[])fp.getPixels()).length);
1819 if (Thread.currentThread().isInterrupted()) return false;
1821 // 0 - blur the previous image to 0.75 sigma
1822 if (0 != k) { // not doing so at the end because it would add one unnecessary blurring
1823 fp = new FloatProcessorT2(sw, sh, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])fp.getPixels(), sw, sh), 0.75f).data, cm);
1824 if (null != alpha) {
1825 alpha = new FloatProcessorT2(sw, sh, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])alpha.getPixels(), sw, sh), 0.75f).data, null);
1826 if (alpha != outside && outside != null) {
1827 outside = new FloatProcessorT2(sw, sh, ImageFilter.computeGaussianFastMirror(new FloatArray2D((float[])outside.getPixels(), sw, sh), 0.75f).data, null);
1831 // 1 - check that the target folder for the desired scale exists
1832 final String target_dir = getLevelDir(dir_mipmaps, k);
1833 if (null == target_dir) continue;
1834 // 2 - generate scaled image
1835 if (0 != k) {
1836 fp = (FloatProcessor)fp.resize(w, h);
1837 if (ImagePlus.GRAY8 == type) {
1838 fp.setMinAndMax(0, 255); // the min and max was expanded into 0,255 range at convertToFloat for 8-bit images, so the only limit to be added now to the FloatProcessor is that of the 8-bit range. The latter is done automatically for FloatProcessor class, but FloatProcessorT2 doesn't, to avoid the expensive (and here superfluous) operation of looping through all pixels in the findMinAndMax method.
1839 } else {
1840 fp.setMinAndMax(patch.getMin(), patch.getMax()); // Must be done: the resize doesn't preserve the min and max!
1842 if (null != alpha) {
1843 alpha = (FloatProcessor)alpha.resize(w, h);
1844 if (alpha != outside && null != outside) {
1845 outside = (FloatProcessor)outside.resize(w, h);
1849 if (null != alpha) {
1850 // 3 - save as jpeg with alpha
1851 final byte[] a = (byte[])alpha.convertToByte(false).getPixels();
1852 if (null != outside) {
1853 final byte[] o;
1854 if (alpha != outside) {
1855 o = (byte[])outside.convertToByte(false).getPixels();
1856 } else {
1857 o = a;
1859 // Remove all not completely inside pixels from the alpha mask
1860 // If there was no alpha mask, alpha is the outside itself
1861 for (int i=0; i<o.length; i++) {
1862 if ( (o[i]&0xff) != 255 ) a[i] = 0; // TODO I am sure there is a bitwise operation to do this in one step. Some thing like: a[i] &= 127;
1865 if (ImagePlus.GRAY8 != type) { // for 8-bit, the min,max has been applied when going to FloatProcessor
1866 fp.setMinAndMax(patch.getMin(), patch.getMax());
1868 final int[] pix = embedAlpha((int[])fp.convertToRGB().getPixels(), a);
1870 final BufferedImage bi_save = createARGBImage(w, h, pix);
1871 if (!ini.trakem2.io.ImageSaver.saveAsJpegAlpha(bi_save, target_dir + filename, 0.85f)) {
1872 cannot_regenerate.add(patch);
1873 bi_save.flush();
1874 break;
1876 bi_save.flush();
1877 } else {
1878 // 3 - save as 8-bit jpeg
1879 final ImageProcessor ip2 = Utils.convertTo(fp, type, false); // no scaling, since the conversion to float above didn't change the range. This is needed because of the min and max
1880 if (!coordinate_transformed) ip2.setMinAndMax(patch.getMin(), patch.getMax()); // Must be done, it's a new ImageProcessor
1881 if (null != cm) ip2.setColorModel(cm); // the LUT
1883 if (!ini.trakem2.io.ImageSaver.saveAsJpeg(ip2, target_dir + filename, 0.85f, as_grey)) {
1884 cannot_regenerate.add(patch);
1885 break;
1889 // 4 - prepare values for the next scaled image
1890 sw = w;
1891 sh = h;
1892 w /= 2;
1893 h /= 2;
1894 k++;
1895 } while (w >= 32 && h >= 32); // not smaller than 32x32
1897 } else {
1898 //final StopWatch timer = new StopWatch();
1900 // use java hardware-accelerated resizing
1901 Image awt = ip.createImage();
1903 BufferedImage balpha = null == alpha_mask ? null : convertToBufferedImage(alpha_mask);
1904 BufferedImage boutside = null == outside_mask ? null : convertToBufferedImage(outside_mask);
1906 BufferedImage bi = null;
1907 final Object hint = getHint(resizing_mode);
1909 ip = null;
1911 do {
1912 if (Thread.currentThread().isInterrupted()) return false;
1914 // check that the target folder for the desired scale exists
1915 final String target_dir = getLevelDir(dir_mipmaps, k);
1916 if (null == target_dir) continue;
1917 // obtain half image
1918 // for level 0 and others, when awt is not a BufferedImage or needs to be reduced in size (to new w,h)
1919 final BufferedImage[] res = IToBI(awt, w, h, hint, balpha, boutside);
1920 bi = res[0];
1921 balpha = res[1];
1922 boutside = res[2];
1923 // prepare next iteration
1924 if (awt != bi) awt.flush();
1925 awt = bi;
1926 w /= 2;
1927 h /= 2;
1928 k++;
1929 // save this iteration
1930 if ( ( (null != balpha || null != boutside) &&
1931 !ini.trakem2.io.ImageSaver.saveAsJpegAlpha(bi, target_dir + filename, 0.85f))
1932 || ( null == balpha && null == boutside && !ini.trakem2.io.ImageSaver.saveAsJpeg(bi, target_dir + filename, 0.85f, as_grey))) {
1933 cannot_regenerate.add(patch);
1934 break;
1936 } while (w >= 32 && h >= 32);
1937 bi.flush();
1939 //timer.cumulative();
1943 // flush any cached tiles
1944 flushMipMaps(patch.getId());
1946 return true;
1947 } catch (Throwable e) {
1948 IJError.print(e);
1949 cannot_regenerate.add(patch);
1950 return false;
1951 } finally {
1952 // gets executed even when returning from the catch statement or within the try/catch block
1953 synchronized (gm_lock) {
1954 gm_lock();
1955 hs_regenerating_mipmaps.remove(patch);
1956 gm_unlock();
1961 /** Remove the file, if it exists, with serialized features for patch.
1962 * Returns true when no such file or on success; false otherwise. */
1963 public boolean removeSerializedFeatures(final Patch patch) {
1964 final File f = new File(new StringBuffer(getUNUIdFolder()).append("features.ser/").append(FSLoader.createIdPath(Long.toString(patch.getId()), "features", ".ser")).toString());
1965 if (f.exists()) {
1966 try {
1967 return f.delete();
1968 } catch (Exception e) {
1969 IJError.print(e);
1970 return false;
1972 } else return true;
1975 /** Remove the file, if it exists, with serialized point matches for patch.
1976 * Returns true when no such file or on success; false otherwise. */
1977 public boolean removeSerializedPointMatches(final Patch patch) {
1978 final String ser = new StringBuffer(getUNUIdFolder()).append("pointmatches.ser/").toString();
1979 final File fser = new File(ser);
1981 if (!fser.exists() || !fser.isDirectory()) return true;
1983 boolean success = true;
1984 final String sid = Long.toString(patch.getId());
1986 final ArrayList<String> removed_paths = new ArrayList<String>();
1988 // 1 - Remove all files with <p1.id>_<p2.id>:
1989 if (sid.length() < 2) {
1990 // Delete all files starting with sid + '_' and present directly under fser
1991 success = Utils.removePrefixedFiles(fser, sid + "_", removed_paths);
1992 } else {
1993 final String sid_ = sid + "_"; // minimal 2 length: a number and the underscore
1994 final int len = sid_.length();
1995 final StringBuffer dd = new StringBuffer();
1996 for (int i=1; i<=len; i++) {
1997 dd.append(sid_.charAt(i-1));
1998 if (0 == i % 2 && len != i) dd.append('/');
2000 final String med = dd.toString();
2001 final int last_slash = med.lastIndexOf('/');
2002 final File med_parent = new File(ser + med.substring(0, last_slash+1));
2003 // case of 12/34/_* ---> use prefix: "_"
2004 // case of 12/34/5_/* ---> use prefix: last number plus underscore, aka: med.substring(med.length()-2);
2005 success = Utils.removePrefixedFiles(med_parent,
2006 last_slash == med.length() -2 ? "_" : med.substring(med.length() -2),
2007 removed_paths);
2010 // 2 - For each removed path, find the complementary: <*>_<p1.id>
2011 for (String path : removed_paths) {
2012 if (IJ.isWindows()) path = path.replace('\\', '/');
2013 File f = new File(path);
2014 // Check that its a pointmatches file
2015 int idot = path.lastIndexOf(".pointmatches.ser");
2016 if (idot < 0) {
2017 Utils.log2("Not a pointmatches.ser file: can't process " + path);
2018 continue;
2021 // Find the root
2022 int ifolder = path.indexOf("pointmatches.ser/");
2023 if (ifolder < 0) {
2024 Utils.log2("Not in pointmatches.ser/ folder:" + path);
2025 continue;
2027 String dir = path.substring(0, ifolder + 17);
2029 // Cut the beginning and the end
2030 String name = path.substring(dir.length(), idot);
2031 Utils.log2("name: " + name);
2032 // Remove all path separators
2033 name = name.replaceAll("/", "");
2035 int iunderscore = name.indexOf('_');
2036 if (-1 == iunderscore) {
2037 Utils.log2("No underscore: can't process " + path);
2038 continue;
2040 name = FSLoader.createIdPath(new StringBuffer().append(name.substring(iunderscore+1)).append('_').append(name.substring(0, iunderscore)).toString(), "pointmatches", ".ser");
2042 f = new File(dir + name);
2043 if (f.exists()) {
2044 if (!f.delete()) {
2045 Utils.log2("Could not delete " + f.getAbsolutePath());
2046 success = false;
2047 } else {
2048 Utils.log2("Deleted pointmatches file " + name);
2049 // Now remove its parent directories within pointmatches.ser/ directory, if they are empty
2050 int islash = name.lastIndexOf('/');
2051 String dirname = name;
2052 while (islash > -1) {
2053 dirname = dirname.substring(0, islash);
2054 if (!Utils.removeFile(new File(dir + dirname))) {
2055 // directory not empty
2056 break;
2058 islash = dirname.lastIndexOf('/');
2061 } else {
2062 Utils.log2("File does not exist: " + dir + name);
2066 return success;
2069 /** Generate image pyramids and store them into files under the dir_mipmaps for each Patch object in the Project. The method is multithreaded, using as many processors as available to the JVM.
2071 * @param al : the list of Patch instances to generate mipmaps for.
2072 * @param overwrite : whether to overwrite any existing mipmaps, or save only those that don't exist yet for whatever reason. This flag provides the means for minimal effort mipmap regeneration.)
2073 * */
2074 public Bureaucrat generateMipMaps(final ArrayList al, final boolean overwrite) {
2075 if (null == al || 0 == al.size()) return null;
2076 if (null == dir_mipmaps) createMipMapsDir(null);
2077 if (isURL(dir_mipmaps)) {
2078 Utils.log("Mipmaps folder is an URL, can't save files into it.");
2079 return null;
2081 final Worker worker = new Worker("Generating MipMaps") {
2082 public void run() {
2083 this.setAsBackground(true);
2084 this.startedWorking();
2085 try {
2087 final Worker wo = this;
2089 Utils.log2("starting mipmap generation ..");
2091 final int size = al.size();
2092 final Patch[] pa = new Patch[size];
2093 final Thread[] threads = MultiThreading.newThreads();
2094 al.toArray(pa);
2095 final AtomicInteger ai = new AtomicInteger(0);
2097 for (int ithread = 0; ithread < threads.length; ++ithread) {
2098 threads[ithread] = new Thread(new Runnable() {
2099 public void run() {
2101 for (int k = ai.getAndIncrement(); k < size; k = ai.getAndIncrement()) {
2102 if (wo.hasQuitted()) {
2103 return;
2105 wo.setTaskName("Generating MipMaps " + (k+1) + "/" + size);
2106 try {
2107 boolean ow = overwrite;
2108 if (!overwrite) {
2109 // check if all the files exist. If one doesn't, then overwrite all anyway
2110 int w = (int)pa[k].getWidth();
2111 int h = (int)pa[k].getHeight();
2112 int level = 0;
2113 final String filename = new File(getAbsolutePath(pa[k])).getName() + "." + pa[k].getId() + ".jpg";
2114 do {
2115 w /= 2;
2116 h /= 2;
2117 level++;
2118 if (!new File(dir_mipmaps + level + "/" + filename).exists()) {
2119 ow = true;
2120 break;
2122 } while (w >= 32 && h >= 32);
2124 if (!ow) continue;
2125 if ( ! generateMipMaps(pa[k]) ) {
2126 // some error ocurred
2127 Utils.log2("Could not generate mipmaps for patch " + pa[k]);
2129 } catch (Exception e) {
2130 IJError.print(e);
2137 MultiThreading.startAndJoin(threads);
2139 } catch (Exception e) {
2140 IJError.print(e);
2143 this.finishedWorking();
2146 return Bureaucrat.createAndStart(worker, ((Patch)al.get(0)).getProject());
2149 private final String getLevelDir(final String dir_mipmaps, final int level) {
2150 // synch, so that multithreaded generateMipMaps won't collide trying to create dirs
2151 synchronized (db_lock) {
2152 lock();
2153 final String path = new StringBuffer(dir_mipmaps).append(level).append('/').toString();
2154 if (isURL(dir_mipmaps)) {
2155 unlock();
2156 return path;
2158 final File file = new File(path);
2159 if (file.exists() && file.isDirectory()) {
2160 unlock();
2161 return path;
2163 // else, create it
2164 try {
2165 file.mkdir();
2166 unlock();
2167 return path;
2168 } catch (Exception e) {
2169 IJError.print(e);
2171 unlock();
2173 return null;
2176 /** Returns the near-unique folder for the project hosted by this FSLoader. */
2177 public String getUNUIdFolder() {
2178 return new StringBuffer(getStorageFolder()).append("trakem2.").append(unuid).append('/').toString();
2181 private String obtainUNUIdFolder() {
2182 YesNoCancelDialog yn = ControlWindow.makeYesNoCancelDialog("Old .xml version!", "The loaded XML file does not contain an UNUId. Select a UNUId folder?\n:Should look like: trakem2.12345678.12345678.12345678");
2183 if (!yn.yesPressed()) return null;
2184 DirectoryChooser dc = new DirectoryChooser("Select UNUId folder");
2185 String unuid_dir = dc.getDirectory();
2186 if (null != unuid_dir) {
2187 unuid_dir = unuid_dir.replace('\\', '/');
2188 if (!unuid_dir.startsWith("trakem2.")) {
2189 Utils.logAll("Invalid UNUId folder. Try again or cancel.");
2190 return obtainUNUIdFolder();
2191 } else {
2192 String[] nums = unuid_dir.split("\\.");
2193 if (nums.length != 4) {
2194 Utils.logAll("Invalid UNUId folder. Try again or cancel.");
2195 return obtainUNUIdFolder();
2197 for (int i=1; i<nums.length; i++) {
2198 try {
2199 long num = Long.parseLong(nums[i]);
2200 } catch (NumberFormatException nfe) {
2201 Utils.logAll("Invalid UNUId folder. Try again or cancel.");
2202 return obtainUNUIdFolder();
2205 // ok, aceptamos pulpo
2206 this.unuid = unuid_dir.substring(8);
2207 if (unuid_dir.lastIndexOf('/') != unuid_dir.length() -1) {
2208 this.unuid = this.unuid.substring(0, this.unuid.length() -1);
2209 } else {
2210 unuid_dir += "/";
2212 this.dir_mipmaps = unuid_dir + "trakem2.mipmaps/";
2215 return null;
2218 /** If parent path is null, it's asked for.*/
2219 private boolean createMipMapsDir(String parent_path) {
2220 if (null == this.unuid) this.unuid = createUNUId(parent_path);
2221 if (null == parent_path) {
2222 // try to create it in the same directory where the XML file is
2223 if (null != dir_storage) {
2224 File f = new File(getUNUIdFolder() + "/trakem2.mipmaps");
2225 if (!f.exists()) {
2226 try {
2227 if (f.mkdir()) {
2228 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
2229 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
2230 return true;
2232 } catch (Exception e) {}
2233 } else if (f.isDirectory()) {
2234 this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
2235 if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
2236 return true;
2238 // else can't use it
2240 // else, ask for a new folder
2241 final DirectoryChooser dc = new DirectoryChooser("Select MipMaps parent directory");
2242 parent_path = dc.getDirectory();
2243 if (null == parent_path) return false;
2244 parent_path = parent_path.replace('\\', '/');
2245 if (!parent_path.endsWith("/")) parent_path += "/";
2247 // examine parent path
2248 final File file = new File(parent_path);
2249 if (file.exists()) {
2250 if (file.isDirectory()) {
2251 // all OK
2252 this.dir_mipmaps = parent_path + "trakem2." + unuid + "/trakem2.mipmaps/";
2253 try {
2254 File f = new File(this.dir_mipmaps);
2255 f.mkdirs();
2256 if (!f.exists()) {
2257 Utils.log("Could not create trakem2.mipmaps!");
2258 return false;
2260 } catch (Exception e) {
2261 IJError.print(e);
2262 return false;
2264 } else {
2265 Utils.showMessage("Selected parent path is not a directory. Please choose another one.");
2266 return createMipMapsDir(null);
2268 } else {
2269 Utils.showMessage("Parent path does not exist. Please select a new one.");
2270 return createMipMapsDir(null);
2272 return true;
2275 /** Remove all mipmap images from the cache, and optionally set the dir_mipmaps to null. */
2276 public void flushMipMaps(boolean forget_dir_mipmaps) {
2277 if (null == dir_mipmaps) return;
2278 synchronized (db_lock) {
2279 lock();
2280 if (forget_dir_mipmaps) this.dir_mipmaps = null;
2281 mawts.removeAllPyramids(); // does not remove level 0 awts (i.e. the 100% images)
2282 unlock();
2286 /** Remove from the cache all images of level larger than zero corresponding to the given patch id. */
2287 public void flushMipMaps(final long id) {
2288 if (null == dir_mipmaps) return;
2289 synchronized (db_lock) {
2290 lock();
2291 try {
2292 //mawts.removePyramid(id); // does not remove level 0 awts (i.e. the 100% images)
2293 // Need to remove ALL now, since level 0 is also included as a mipmap:
2294 for (final Image img : mawts.remove(id)) {
2295 if (null != img) img.flush();
2297 } catch (Exception e) { e.printStackTrace(); }
2298 unlock();
2302 /** Gets data from the Patch and queues a new task to do the file removal in a separate task manager thread. */
2303 public void removeMipMaps(final Patch p) {
2304 if (null == dir_mipmaps) return;
2305 try {
2306 final int width = (int)p.getWidth();
2307 final int height = (int)p.getHeight();
2308 final String path = getAbsolutePath(p);
2309 if (null == path) return; // missing file
2310 final String filename = new File(path).getName() + "." + p.getId() + ".jpg";
2311 // cue the task in a dispatcher:
2312 dispatcher.exec(new Runnable() { public void run() { // copy-paste as a replacement for (defmacro ... we luv java
2313 removeMipMaps(createIdPath(Long.toString(p.getId()), filename, ".jpg"), width, height);
2314 }});
2315 } catch (Exception e) {
2316 IJError.print(e);
2320 private void removeMipMaps(final String filename, final int width, final int height) {
2321 int w = width;
2322 int h = height;
2323 int k = 0; // the level
2324 do {
2325 final File f = new File(dir_mipmaps + k + "/" + filename);
2326 if (f.exists()) {
2327 try {
2328 if (!f.delete()) {
2329 Utils.log2("Could not remove file " + f.getAbsolutePath());
2331 } catch (Exception e) {
2332 IJError.print(e);
2335 w /= 2;
2336 h /= 2;
2337 k++;
2338 } while (w >= 32 && h >= 32); // not smaller than 32x32
2341 /** Checks whether this Loader is using a directory of image pyramids for each Patch or not. */
2342 public boolean isMipMapsEnabled() {
2343 return null != dir_mipmaps;
2346 /** Return the closest level to @param level that exists as a file.
2347 * If no valid path is found for the patch, returns ERROR_PATH_NOT_FOUND.
2349 public int getClosestMipMapLevel(final Patch patch, int level) {
2350 if (null == dir_mipmaps) return 0;
2351 try {
2352 final String path = getAbsolutePath(patch);
2353 if (null == path) return ERROR_PATH_NOT_FOUND;
2354 final String filename = new File(path).getName() + ".jpg";
2355 if (isURL(dir_mipmaps)) {
2356 if (level <= 0) return 0;
2357 // choose the smallest dimension
2358 // find max level that keeps dim over 32 pixels
2359 final int lev = getHighestMipMapLevel(Math.min(patch.getWidth(), patch.getHeight()));
2360 if (level > lev) return lev;
2361 return level;
2362 } else {
2363 do {
2364 final File f = new File(new StringBuffer(dir_mipmaps).append(level).append('/').append(filename).toString());
2365 if (f.exists()) {
2366 return level;
2368 // try the next level
2369 level--;
2370 } while (level >= 0);
2372 } catch (Exception e) {
2373 IJError.print(e);
2375 return 0;
2378 /** A temporary list of Patch instances for which a pyramid is being generated. */
2379 final private HashSet hs_regenerating_mipmaps = new HashSet();
2381 /** A lock for the generation of mipmaps. */
2382 final private Object gm_lock = new Object();
2383 private boolean gm_locked = false;
2385 protected final void gm_lock() {
2386 //Utils.printCaller(this, 7);
2387 while (gm_locked) { try { gm_lock.wait(); } catch (InterruptedException ie) {} }
2388 gm_locked = true;
2390 protected final void gm_unlock() {
2391 //Utils.printCaller(this, 7);
2392 if (gm_locked) {
2393 gm_locked = false;
2394 gm_lock.notifyAll();
2398 /** Checks if the mipmap file for the Patch and closest upper level to the desired magnification exists. */
2399 public boolean checkMipMapFileExists(final Patch p, final double magnification) {
2400 if (null == dir_mipmaps) return false;
2401 final int level = getMipMapLevel(magnification, maxDim(p));
2402 if (isURL(dir_mipmaps)) return true; // just assume that it does
2403 if (new File(dir_mipmaps + level + "/" + new File(getAbsolutePath(p)).getName() + "." + p.getId() + ".jpg").exists()) return true;
2404 return false;
2407 final Set<Patch> cannot_regenerate = Collections.synchronizedSet(new HashSet<Patch>());
2409 /** Loads the file containing the scaled image corresponding to the given level (or the maximum possible level, if too large) and returns it as an awt.Image, or null if not found. Will also regenerate the mipmaps, i.e. recreate the pre-scaled jpeg images if they are missing. Does not frees memory on its own. */
2410 protected Image fetchMipMapAWT(final Patch patch, final int level) {
2411 if (null == dir_mipmaps) {
2412 Utils.log2("null dir_mipmaps");
2413 return null;
2415 try {
2416 // TODO should wait if the file is currently being generated
2417 // (it's somewhat handled by a double-try to open the jpeg image)
2419 final int max_level = getHighestMipMapLevel(patch);
2421 //Utils.log2("level is: " + max_level);
2423 final String filename = getInternalFileName(patch);
2424 if (null == filename) {
2425 Utils.log2("null internal filename!");
2426 return null;
2428 // Old style:
2429 //final String path = new StringBuffer(dir_mipmaps).append( level > max_level ? max_level : level ).append('/').append(filename).append('.').append(patch.getId()).append(".jpg").toString();
2430 // New style:
2431 final String path = new StringBuffer(dir_mipmaps).append( level > max_level ? max_level : level ).append('/').append(createIdPath(Long.toString(patch.getId()), filename, ".jpg")).toString();
2433 Image img = null;
2435 if (patch.hasAlphaChannel()) {
2436 img = ImageSaver.openJpegAlpha(path);
2437 } else {
2438 switch (patch.getType()) {
2439 case ImagePlus.GRAY16:
2440 case ImagePlus.GRAY8:
2441 case ImagePlus.GRAY32:
2442 img = ImageSaver.openGreyJpeg(path);
2443 break;
2444 default:
2445 IJ.redirectErrorMessages();
2446 ImagePlus imp = opener.openImage(path); // considers URL as well
2447 if (null != imp) return patch.createImage(imp); // considers c_alphas
2448 //img = patch.adjustChannels(Toolkit.getDefaultToolkit().createImage(path)); // doesn't work
2449 //img = patch.adjustChannels(ImageSaver.openColorJpeg(path)); // doesn't work
2450 //Utils.log2("color jpeg path: "+ path);
2451 //Utils.log2("exists ? " + new File(path).exists());
2452 break;
2455 if (null != img) return img;
2458 // if we got so far ... try to regenerate the mipmaps
2459 if (!mipmaps_regen) {
2460 return null;
2463 // check that REALLY the file doesn't exist.
2464 if (cannot_regenerate.contains(patch)) {
2465 Utils.log("Cannot regenerate mipmaps for patch " + patch);
2466 return null;
2469 //Utils.log2("getMipMapAwt: imp is " + imp + " for path " + dir_mipmaps + level + "/" + new File(getAbsolutePath(patch)).getName() + "." + patch.getId() + ".jpg");
2471 // Regenerate in the case of not asking for an image under 32x32
2472 double scale = 1 / Math.pow(2, level);
2473 if (level >= 0 && patch.getWidth() * scale >= 32 && patch.getHeight() * scale >= 32 && isMipMapsEnabled()) {
2474 // regenerate
2475 regenerateMipMaps(patch);
2476 return REGENERATING;
2478 } catch (Exception e) {
2479 IJError.print(e);
2481 return null;
2484 static private AtomicInteger n_regenerating = new AtomicInteger(0);
2485 static private ExecutorService regenerator = null;
2486 static public ExecutorService repainter = null;
2488 /** Queue the regeneration of mipmaps for the Patch; returns immediately, having submitted the job to an executor queue;
2489 * returns true if the task was submitted, false if not. */
2490 public final boolean regenerateMipMaps(final Patch patch) {
2491 synchronized (gm_lock) {
2492 try {
2493 gm_lock();
2494 if (hs_regenerating_mipmaps.contains(patch)) {
2495 return false;
2497 // else, start it
2498 hs_regenerating_mipmaps.add(patch);
2499 } catch (Exception e) {
2500 IJError.print(e);
2501 return false;
2502 } finally {
2503 gm_unlock();
2506 try {
2507 n_regenerating.incrementAndGet();
2508 Utils.log2("SUBMITTING to regen " + patch);
2509 regenerator.submit(new Runnable() {
2510 public void run() {
2511 try {
2512 Utils.showStatus("Regenerating mipmaps (" + n_regenerating.get() + " to go)");
2513 generateMipMaps(patch, false);
2514 Display.repaint(patch.getLayer());
2515 Utils.showStatus("");
2516 } catch (Exception e) {
2517 IJError.print(e);
2519 n_regenerating.decrementAndGet();
2522 return true;
2523 } catch (Exception e) {
2524 IJError.print(e);
2525 ThreadPoolExecutor tpe = (ThreadPoolExecutor) regenerator;
2526 Utils.log2("active thread count: " + tpe.getActiveCount() +
2527 "\ncore pool size: " + tpe.getCorePoolSize() +
2528 "\ncompleted: " + tpe.getCompletedTaskCount() +
2529 "\nqueued: " + tpe.getQueue().size() +
2530 "\ntask count: " + tpe.getTaskCount());
2533 return false;
2536 /** Compute the number of bytes that the ImagePlus of a Patch will take. Assumes a large header of 1024 bytes. If the image is saved as a grayscale jpeg the returned bytes will be 5 times as expected, because jpeg images are opened as int[] and then copied to a byte[] if all channels have the same values for all pixels. */ // The header is unnecessary because it's read, but not stored except for some of its variables; it works here as a safety buffer space.
2537 public long estimateImageFileSize(final Patch p, final int level) {
2538 if (level > 0) {
2539 // jpeg image to be loaded:
2540 final double scale = 1 / Math.pow(2, level);
2541 return (long)(p.getWidth() * scale * p.getHeight() * scale * 5 + 1024);
2543 long size = (long)(p.getWidth() * p.getHeight());
2544 int bytes_per_pixel = 1;
2545 final int type = p.getType();
2546 switch (type) {
2547 case ImagePlus.GRAY32:
2548 bytes_per_pixel = 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
2549 break;
2550 case ImagePlus.GRAY16:
2551 bytes_per_pixel = 3; // 2 for the ShortProcessor, and 1 for the pixels8
2552 case ImagePlus.COLOR_RGB:
2553 bytes_per_pixel = 4;
2554 break;
2555 case ImagePlus.GRAY8:
2556 case ImagePlus.COLOR_256:
2557 bytes_per_pixel = 1;
2558 // check jpeg, which can only encode RGB (taken care of above) and 8-bit and 8-bit color images:
2559 String path = ht_paths.get(p.getId());
2560 if (null != path && path.endsWith(".jpg")) bytes_per_pixel = 5; //4 for the int[] and 1 for the byte[]
2561 break;
2562 default:
2563 bytes_per_pixel = 5; // conservative
2564 break;
2567 return size * bytes_per_pixel + 1024;
2570 public String makeProjectName() {
2571 if (null == project_file_path || 0 == project_file_path.length()) return super.makeProjectName();
2572 final String name = new File(project_file_path).getName();
2573 final int i_dot = name.lastIndexOf('.');
2574 if (-1 == i_dot) return name;
2575 if (0 == i_dot) return super.makeProjectName();
2576 return name.substring(0, i_dot);
2580 /** Returns the path where the imp is saved to: the storage folder plus a name. */
2581 public String handlePathlessImage(final ImagePlus imp) {
2582 final FileInfo fi = imp.getOriginalFileInfo();
2583 if (null == fi.fileName || fi.fileName.equals("")) {
2584 fi.fileName = "img_" + System.currentTimeMillis() + ".tif";
2586 if (!fi.fileName.endsWith(".tif")) fi.fileName += ".tif";
2587 fi.directory = dir_storage;
2588 if (imp.getNSlices() > 1) {
2589 new FileSaver(imp).saveAsTiffStack(dir_storage + fi.fileName);
2590 } else {
2591 new FileSaver(imp).saveAsTiff(dir_storage + fi.fileName);
2593 Utils.log2("Saved a copy into the storage folder:\n" + dir_storage + fi.fileName);
2594 return dir_storage + fi.fileName;
2597 /** Generates layer-wise mipmaps with constant tile width and height. The mipmaps include only images.
2598 * Mipmaps area generated all the way down until the entire canvas fits within one single tile.
2600 public Bureaucrat generateLayerMipMaps(final Layer[] la, final int starting_level) {
2601 // hard-coded dimensions for layer mipmaps.
2602 final int WIDTH = 512;
2603 final int HEIGHT = 512;
2605 // Each tile needs some coding system on where it belongs. For example in its file name, such as <layer_id>_Xi_Yi
2607 // Generate the starting level mipmaps, and then the others from it by gaussian or whatever is indicated in the project image_resizing_mode property.
2608 return null;
2611 /** Convert old-style storage folders to new style. */
2612 public boolean fixStorageFolders() {
2613 try {
2614 // 1 - Create folder unuid_folder at storage_folder + unuid
2615 if (null == this.unuid) {
2616 Utils.log2("No unuid for project!");
2617 return false;
2619 // the trakem2.<unuid> folder that will now contain trakem2.mipmaps, trakem2.masks, etc.
2620 final String unuid_folder = getUNUIdFolder();
2621 File fdir = new File(unuid_folder);
2622 if (!fdir.exists()) {
2623 if (!fdir.mkdir()) {
2624 Utils.log2("Could not create folder " + unuid_folder);
2625 return false;
2628 // 2 - Create trakem2.mipmaps inside unuid folder
2629 final String new_dir_mipmaps = unuid_folder + "trakem2.mipmaps/";
2630 fdir = new File(new_dir_mipmaps);
2631 if (!fdir.mkdir()) {
2632 Utils.log2("Could not create folder " + new_dir_mipmaps);
2633 return false;
2635 // 3 - Reorganize current mipmaps folder to folders with following convention: <level>/dd/dd/d.jpg where ddddd is Patch.id=12345 12/34/5.jpg etc.
2636 final String dir_mipmaps = getMipMapsFolder();
2637 for (final String name : new File(dir_mipmaps).list()) {
2638 String level_dir = new StringBuffer(dir_mipmaps).append(name).append('/').toString();
2639 final File f = new File(level_dir);
2640 if (!f.isDirectory() || f.isHidden()) continue;
2641 for (final String mm : f.list()) {
2642 if (!mm.endsWith(".jpg")) continue;
2643 // parse the mipmap file: filename + '.' + id + '.jpg'
2644 int last_dot = mm.lastIndexOf('.');
2645 if (-1 == last_dot) continue;
2646 int prev_last_dot = mm.lastIndexOf('.', last_dot -1);
2647 String id = mm.substring(prev_last_dot+1, last_dot);
2648 String filename = mm.substring(0, prev_last_dot);
2649 File oldf = new File(level_dir + mm);
2650 File newf = new File(new StringBuffer(new_dir_mipmaps).append(name).append('/').append(createIdPath(id, filename, ".jpg")).toString());
2651 File fd = newf.getParentFile();
2652 fd.mkdirs();
2653 if (!fd.exists()) {
2654 Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
2655 continue;
2657 if (!oldf.renameTo(newf)) {
2658 Utils.log2("Could not move mipmap file " + oldf.getAbsolutePath() + " to " + newf.getAbsolutePath());
2659 continue;
2663 // Set it!
2664 this.dir_mipmaps = new_dir_mipmaps;
2666 // Remove old empty dirs:
2667 Utils.removeFile(new File(dir_mipmaps));
2669 // 4 - same for alpha folder and features folder.
2670 final String masks_folder = getStorageFolder() + "trakem2.masks/";
2671 File fmasks = new File(masks_folder);
2672 this.dir_masks = null;
2673 if (fmasks.exists()) {
2674 final String new_dir_masks = unuid_folder + "trakem2.masks/";
2675 for (final File fmask : fmasks.listFiles()) {
2676 final String name = fmask.getName();
2677 if (!name.endsWith(".zip")) continue;
2678 int last_dot = name.lastIndexOf('.');
2679 if (-1 == last_dot) continue;
2680 int prev_last_dot = name.lastIndexOf('.', last_dot -1);
2681 String id = name.substring(prev_last_dot+1, last_dot);
2682 String filename = name.substring(0, prev_last_dot);
2683 File newf = new File(new_dir_masks + createIdPath(id, filename, ".zip"));
2684 File fd = newf.getParentFile();
2685 fd.mkdirs();
2686 if (!fd.exists()) {
2687 Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
2688 continue;
2690 if (!fmask.renameTo(newf)) {
2691 Utils.log2("Could not move mask file " + fmask.getAbsolutePath() + " to " + newf.getAbsolutePath());
2692 continue;
2695 // Set it!
2696 this.dir_masks = new_dir_masks;
2698 // remove old empty:
2699 Utils.removeFile(fmasks);
2702 // TODO should save the .xml file, so the unuid and the new storage folders are set in there!
2704 return true;
2705 } catch (Exception e) {
2706 IJError.print(e);
2708 return false;
2711 /** For Patch id=12345 creates 12/34/5.${filename}.jpg */
2712 static public final String createMipMapRelPath(final Patch p) {
2713 return createIdPath(Long.toString(p.getId()), new File(p.getCurrentPath()).getName(), ".jpg");
2716 /** For sid=12345 creates 12/34/5.${filename}.jpg
2717 * Will be fine with other filename-valid chars in sid. */
2718 static public final String createIdPath(final String sid, final String filename, final String ext) {
2719 final StringBuffer sf = new StringBuffer(((sid.length() * 3) / 2) + 1);
2720 final int len = sid.length();
2721 for (int i=1; i<=len; i++) {
2722 sf.append(sid.charAt(i-1));
2723 if (0 == i % 2 && len != i) sf.append('/');
2725 return sf.append('.').append(filename).append(ext).toString();
2728 public String getUNUId() {
2729 return unuid;