/**
TrakEM2 plugin for ImageJ(C).
Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
/s published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
You may contact Albert Cardona at acardona at ini.phys.ethz.ch
Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
**/
package ini.trakem2.persistence;
import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.VirtualStack;
import ij.gui.YesNoCancelDialog;
import ij.io.DirectoryChooser;
import ij.io.FileInfo;
import ij.io.FileSaver;
import ij.io.OpenDialog;
import ij.io.Opener;
import ij.plugin.filter.GaussianBlur;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import ini.trakem2.ControlWindow;
import ini.trakem2.Project;
import ini.trakem2.display.DLabel;
import ini.trakem2.display.Display;
import ini.trakem2.display.Displayable;
import ini.trakem2.display.Layer;
import ini.trakem2.display.MipMapImage;
import ini.trakem2.display.Patch;
import ini.trakem2.display.Stack;
import ini.trakem2.imaging.FloatProcessorT2;
import ini.trakem2.imaging.P;
import ini.trakem2.io.ImageSaver;
import ini.trakem2.io.RagMipMaps;
import ini.trakem2.io.RawMipMaps;
import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.CachingThread;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import mpicbg.trakem2.transform.CoordinateTransform;
import net.imglib2.img.Img;
import net.imglib2.img.array.ArrayImgs;
import net.imglib2.img.imageplus.FloatImagePlus;
import net.imglib2.img.imageplus.ImagePlusImgs;
import net.imglib2.type.numeric.real.FloatType;
import org.janelia.intensity.LinearIntensityMap;
import org.xml.sax.InputSource;
/** 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. */
public final class FSLoader extends Loader {
/* sigma of the Gaussian kernel sto be used for downsampling by a factor of 2 */
final private static double SIGMA_2 = Math.sqrt( 0.75 );
/** Largest id seen so far. */
private long max_id = -1;
/** Largest blob ID seen so far. First valid ID will equal 1. */
private long max_blob_id = 0;
private final Map<Long,String> ht_paths = Collections.synchronizedMap(new HashMap<Long,String>());
/** For saving and overwriting. */
private String project_file_path = null;
/** Path to the directory hosting the file image pyramids. */
private String dir_mipmaps = null;
/** Path to the directory the user provided when creating the project. */
private String dir_storage = null;
/** Path to the directory hosting the alpha masks. */
private String dir_masks = null;
/** Path to dir_storage + "trakem2.images/" */
private String dir_image_storage = null;
private Set<Patch> touched_mipmaps = Collections.synchronizedSet(new HashSet<Patch>());
private Set<Patch> mipmaps_to_remove = Collections.synchronizedSet(new HashSet<Patch>());
/** Used to open a project from an existing XML file. */
public FSLoader() {
super(); // register
FSLoader.startStaticServices();
}
private String unuid = null;
/** Used to create a new project, NOT from an XML file.
* Throws an Exception if the loader cannot read and write to the storage folder. */
public FSLoader(final String storage_folder) throws Exception {
this();
if (null == storage_folder) this.dir_storage = super.getStorageFolder(); // home dir
else this.dir_storage = storage_folder;
this.dir_storage = this.dir_storage.replace('\\', '/');
if (!this.dir_storage.endsWith("/")) this.dir_storage += "/";
if (!Loader.canReadAndWriteTo(dir_storage)) {
Utils.log("WARNING can't read/write to the storage_folder at " + dir_storage);
throw new Exception("Can't write to storage folder " + dir_storage);
} else {
this.unuid = createUNUId(this.dir_storage);
createMipMapsDir(this.dir_storage);
crashDetector();
}
}
private String createUNUId(String dir_storage) {
synchronized (db_lock) {
try {
if (null == dir_storage) dir_storage = System.getProperty("user.dir") + "/";
return new StringBuilder(64).append(System.currentTimeMillis()).append('.')
.append(Math.abs(dir_storage.hashCode())).append('.')
.append(Math.abs(System.getProperty("user.name").hashCode()))
.toString();
} catch (Exception e) {
IJError.print(e);
}
}
return null;
}
/** 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. */
private void crashDetector() {
if (null == dir_mipmaps) {
Utils.log2("Could NOT create crash detection system: null dir_mipmaps.");
return;
}
File f = new File(dir_mipmaps + ".open.t2");
Utils.log2("Crash detector file is " + dir_mipmaps + ".open.t2");
try {
if (f.exists()) {
// crashed!
notifyMipMapsOutOfSynch();
} else {
if (!f.createNewFile() && !dir_mipmaps.startsWith("http:")) {
Utils.showMessage("WARNING: could NOT create crash detection system:\nCannot write to mipmaps folder.");
} else {
Utils.log2("Created crash detection system.");
}
}
} catch (Exception e) {
Utils.log2("Crash detector error:" + e);
IJError.print(e);
}
}
public String getProjectXMLPath() {
if (null == project_file_path) return null;
return project_file_path.toString(); // a copy of it
}
/** 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. */
public String getStorageFolder() {
if (null == dir_storage) return super.getStorageFolder(); // the user's home
return dir_storage.toString(); // a copy
}
/** Returns a folder proven to be writable for images can be stored into. */
public String getImageStorageFolder() {
if (null == dir_image_storage) {
String s = getUNUIdFolder() + "trakem2.images/";
File f = new File(s);
if (f.exists() && f.isDirectory() && f.canWrite()) {
dir_image_storage = s;
return dir_image_storage;
}
else {
try {
f.mkdirs();
dir_image_storage = s;
} catch (Exception e) {
e.printStackTrace();
return getStorageFolder(); // fall back
}
}
}
return dir_image_storage;
}
/** Returns TMLHandler.getProjectData() . If the path is null it'll be asked for. */
public Object[] openFSProject(String path, final boolean open_displays) {
// clean path of double-slashes, safely (and painfully)
if (null != path) {
path = path.replace('\\','/');
path = path.trim();
int itwo = path.indexOf("//");
while (-1 != itwo) {
if (0 == itwo /* samba disk */
|| (5 == itwo && "http:".equals(path.substring(0, 5)))) {
// do nothing
} else {
path = path.substring(0, itwo) + path.substring(itwo+1);
}
itwo = path.indexOf("//", itwo+1);
}
}
//
if (null == path) {
OpenDialog od = new OpenDialog("Select Project", OpenDialog.getDefaultDirectory(), null);
String file = od.getFileName();
if (null == file || file.toLowerCase().startsWith("null")) return null;
String dir = od.getDirectory().replace('\\', '/');
if (!dir.endsWith("/")) dir += "/";
this.project_file_path = dir + file;
Utils.log2("project file path 1: " + this.project_file_path);
} else {
this.project_file_path = path;
Utils.log2("project file path 2: " + this.project_file_path);
}
Utils.log2("Loader.openFSProject: path is " + path);
// check if any of the open projects uses the same file path, and refuse to open if so:
if (null != FSLoader.getOpenProject(project_file_path, this)) {
Utils.showMessage("The project is already open.");
return null;
}
Object[] data = null;
// parse file, according to expected format as indicated by the extension:
final String lcFilePath = this.project_file_path.toLowerCase();
if (lcFilePath.matches(".*(\\.xml|\\.xml\\.gz)")) {
InputStream i_stream = null;
TMLHandler handler = new TMLHandler(this.project_file_path, this);
if (handler.isUnreadable()) {
handler = null;
} else {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setValidating(false);
factory.setXIncludeAware(false);
SAXParser parser = factory.newSAXParser();
if (isURL(this.project_file_path)) {
i_stream = new java.net.URL(this.project_file_path).openStream();
} else {
i_stream = new BufferedInputStream(new FileInputStream(this.project_file_path));
}
if (lcFilePath.endsWith(".gz")) {
i_stream = new GZIPInputStream(i_stream);
}
InputSource input_source = new InputSource(i_stream);
parser.parse(input_source, handler);
} catch (java.io.FileNotFoundException fnfe) {
Utils.log("ERROR: File not found: " + path);
handler = null;
} catch (Exception e) {
IJError.print(e);
handler = null;
} finally {
if (null != i_stream) {
try {
i_stream.close();
} catch (Exception e) {
IJError.print(e);
}
}
}
}
if (null == handler) {
Utils.showMessage("Error when reading the project .xml file.");
return null;
}
data = handler.getProjectData(open_displays);
}
if (null == data) {
Utils.showMessage("Error when parsing the project .xml file.");
return null;
}
// else, good
crashDetector();
return data;
}
// Only one thread at a time may access this method.
synchronized static private final Project getOpenProject(final String project_file_path, final Loader caller) {
if (null == v_loaders) return null;
final Loader[] lo = (Loader[])v_loaders.toArray(new Loader[0]); // atomic way to get the list of loaders
for (int i=0; i<lo.length; i++) {
if (lo[i].equals(caller)) continue;
if (lo[i] instanceof FSLoader) {
if (null == ((FSLoader)lo[i]).project_file_path) continue; // not saved
if (((FSLoader)lo[i]).project_file_path.equals(project_file_path)) {
return Project.findProject(lo[i]);
}
}
}
return null;
}
static public final Project getOpenProject(final String project_file_path) {
return getOpenProject(project_file_path, null);
}
static public final int nStaticServiceThreads() {
int np = Runtime.getRuntime().availableProcessors();
// 1 core = 1 thread
// 2 cores = 2 threads
// 3+ cores = cores-1 threads
if (np > 2) np -= 1;
return np;
}
/** Restart the ExecutorService for mipmaps with {@code n_threads}. */
static public final void restartMipMapThreads(final int n_threads) {
if (null != regenerator && !regenerator.isShutdown()) {
regenerator.shutdown();
}
regenerator = Utils.newFixedThreadPool(Math.max(1, n_threads), "regenerator");
Utils.logAll("Restarted mipmap Executor Service for all projects with " + n_threads + " threads.");
}
static private void startStaticServices() {
// Up to nStaticServiceThreads for regenerator and repainter
if (null == regenerator || regenerator.isShutdown()) {
regenerator = Utils.newFixedThreadPool(1, "regenerator");
}
if (null == repainter || repainter.isShutdown()) {
repainter = Utils.newFixedThreadPool(nStaticServiceThreads, "repainter"); // for SnapshotPanel
}
// Maximum 2 threads for removing files
if (null == remover || remover.isShutdown()) {
remover = Utils.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors()), "mipmap remover");
}
// Just one thread for autosaver
if (null == autosaver || autosaver.isShutdown()) autosaver = Executors.newScheduledThreadPool(1);
}
/** Shutdown the various thread pools and disactivate services in general. */
static private void destroyStaticServices() {
if (null != regenerator) regenerator.shutdownNow();
if (null != remover) remover.shutdownNow();
if (null != repainter) repainter.shutdownNow();
if (null != autosaver) autosaver.shutdownNow();
}
@Override
public synchronized void destroy() {
super.destroy();
Utils.showStatus("", false);
// delete mipmap files that where touched and not cleared as saved (i.e. the project was not saved)
touched_mipmaps.addAll(mipmaps_to_remove);
Set<Patch> touched = new HashSet<Patch>();
synchronized (touched_mipmaps) {
touched.addAll(touched_mipmaps);
}
for (final Patch p : touched) {
File f = new File(getAbsolutePath(p)); // with slice info appended
//Utils.log2("File f is " + f);
Utils.log2("Removing mipmaps for " + p);
// Cannot run in the remover: is a daemon, and would be interrupted.
removeMipMaps(createIdPath(Long.toString(p.getId()), f.getName(), mExt), (int)p.getWidth(), (int)p.getHeight());
}
//
// remove empty trakem2.mipmaps folder if any
if (null != dir_mipmaps && !dir_mipmaps.equals(dir_storage)) {
File f = new File(dir_mipmaps);
if (f.isDirectory() && 0 == f.list(new FilenameFilter() {
public boolean accept(File fdir, String name) {
File file = new File(dir_mipmaps + name);
if (file.isHidden() || '.' == name.charAt(0)) return false;
return true;
}
}).length) {
try { f.delete(); } catch (Exception e) { Utils.log("Could not remove empty trakem2.mipmaps directory."); }
}
}
// remove crash detector
try {
File fm = new File(dir_mipmaps + ".open.t2");
if (!fm.delete()) {
Utils.log2("WARNING: could not delete crash detector file .open.t2 from trakem2.mipmaps folder at " + dir_mipmaps);
}
} catch (Exception e) {
Utils.log2("WARNING: crash detector file trakem.mipmaps/.open.t2 may NOT have been deleted.");
IJError.print(e);
}
if (null == ControlWindow.getProjects() || 1 == ControlWindow.getProjects().size()) {
destroyStaticServices();
}
// remove unuid dir if xml_path is empty (i.e. never saved and not opened from an .xml file)
if (null == project_file_path) {
Utils.log2("Removing unuid dir, since project was never saved.");
final File f = new File(getUNUIdFolder());
if (null != dir_mipmaps) Utils.removePrefixedFiles(f, "trakem2.mipmaps", null);
if (null != dir_masks) Utils.removePrefixedFiles(f, "trakem2.masks", null);
Utils.removePrefixedFiles(f, "features.ser", null);
Utils.removePrefixedFiles(f, "pointmatches.ser", null);
// Only if empty:
if (f.isDirectory()) {
try {
if (!f.delete()) {
Utils.log2("Could not delete unuid directory: likely not empty!");
}
} catch (Exception e) {
Utils.log2("Could not delete unuid directory: " + e);
}
}
}
}
/** Get the next unique id, not shared by any other object within the same project. */
@Override
public long getNextId() {
long nid = -1;
synchronized (db_lock) {
nid = ++max_id;
}
return nid;
}
/** Get the next unique id to be used for the {@link Patch}'s {@link CoordinateTransform} or alpha mask. */
@Override
public long getNextBlobId() {
long nid = 0;
synchronized (db_lock) {
nid = ++max_blob_id;
}
return nid;
}
/** Loaded in full from XML file */
public double[][][] fetchBezierArrays(long id) {
return null;
}
/** Loaded in full from XML file */
public ArrayList<?> fetchPipePoints(long id) {
return null;
}
/** Loaded in full from XML file */
public ArrayList<?> fetchBallPoints(long id) {
return null;
}
/** Loaded in full from XML file */
public Area fetchArea(long area_list_id, long layer_id) {
return null;
}
/* Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImagePlus.getProcessor(). */
public ImagePlus fetchImagePlus(final Patch p) {
return (ImagePlus)fetchImage(p, Layer.IMAGEPLUS);
}
/** Fetch the ImageProcessor in a synchronized manner, so that there are no conflicts in retrieving the ImageProcessor for a specific stack slice, for example.
* Note that the min and max is not set -- it's your burden to call setMinAndMax(p.getMin(), p.getMax()) on the returned ImageProcessor. */
public ImageProcessor fetchImageProcessor(final Patch p) {
return (ImageProcessor)fetchImage(p, Layer.IMAGEPROCESSOR);
}
/** So far accepts Layer.IMAGEPLUS and Layer.IMAGEPROCESSOR as format. */
public Object fetchImage(final Patch p, final int format) {
ImagePlus imp = null;
ImageProcessor ip = null;
String slice = null;
String path = null;
long n_bytes = 0;
ImageLoadingLock plock = null;
synchronized (db_lock) {
try {
imp = mawts.get(p.getId());
path = getAbsolutePath(p);
int i_sl = -1;
if (null != path) i_sl = path.lastIndexOf("-----#slice=");
if (-1 != i_sl) {
if (null != imp) {
// check that the stack is large enough (user may have changed it)
final int ia = Integer.parseInt(path.substring(i_sl + 12));
if (ia <= imp.getNSlices()) {
if (null == imp.getStack() || null == imp.getStack().getPixels(ia)) {
// reload (happens when closing a stack that was opened before importing it, and then trying to paint, for example)
mawts.removeImagePlus(p.getId());
imp = null;
} else {
imp.setSlice(ia);
switch (format) {
case Layer.IMAGEPROCESSOR:
ip = imp.getStack().getProcessor(ia);
return ip;
case Layer.IMAGEPLUS:
return imp;
default:
Utils.log("FSLoader.fetchImage: Unknown format " + format);
return null;
}
}
} else {
return null; // beyond bonds!
}
}
}
// for non-stack images
if (null != imp) {
switch (format) {
case Layer.IMAGEPROCESSOR:
return imp.getProcessor();
case Layer.IMAGEPLUS:
return imp;
default:
Utils.log("FSLoader.fetchImage: Unknown format " + format);
return null;
}
}
if (-1 != i_sl) {
slice = path.substring(i_sl);
// set path proper
path = path.substring(0, i_sl);
}
plock = getOrMakeImageLoadingLock(path);
} catch (Throwable t) {
handleCacheError(t);
return null;
}
}
synchronized (plock) {
imp = mawts.get(p.getId());
if (null == imp && !p.isPreprocessed()) {
// Try shared ImagePlus cache
imp = mawts.get(path); // could have been loaded by a different Patch that uses the same path,
// such as other slices of a stack or duplicated images.
if (null != imp) {
mawts.put(p.getId(), imp, (int)Math.max(p.getWidth(), p.getHeight()));
}
}
if (null != imp) {
// was loaded by a different thread, or is shareable
switch (format) {
case Layer.IMAGEPROCESSOR:
if (null != slice) {
return imp.getStack().getProcessor(Integer.parseInt(slice.substring(12)));
} else {
return imp.getProcessor();
}
case Layer.IMAGEPLUS:
if (null != slice) {
imp.setSlice(Integer.parseInt(slice.substring(12)));
}
return imp;
default:
Utils.log("FSLoader.fetchImage: Unknown format " + format);
return null;
}
}
// going to load:
// reserve memory:
n_bytes = estimateImageFileSize(p, 0);
releaseToFit(n_bytes);
imp = openImage(path);
preProcess(p, imp, n_bytes);
synchronized (db_lock) {
try {
if (null == imp) {
if (!hs_unloadable.contains(p)) {
Utils.log("FSLoader.fetchImagePlus: no image exists for patch " + p + " at path " + path);
hs_unloadable.add(p);
}
if (ControlWindow.isGUIEnabled()) {
FilePathRepair.add(p);
}
removeImageLoadingLock(plock);
return null;
}
if (null != slice) {
// set proper active slice
final int ia = Integer.parseInt(slice.substring(12));
imp.setSlice(ia);
if (Layer.IMAGEPROCESSOR == format) ip = imp.getStack().getProcessor(ia); // otherwise creates one new for nothing
} else {
// for non-stack images
// OBSOLETE and wrong //p.putMinAndMax(imp); // non-destructive contrast: min and max -- WRONG, it's destructive for ColorProcessor and ByteProcessor!
// puts the Patch min and max values into the ImagePlus processor.
if (Layer.IMAGEPROCESSOR == format) ip = imp.getProcessor();
}
mawts.put(p.getId(), imp, (int)Math.max(p.getWidth(), p.getHeight()));
// imp is cached, so:
removeImageLoadingLock(plock);
} catch (Exception e) {
IJError.print(e);
}
switch (format) {
case Layer.IMAGEPROCESSOR:
return ip; // not imp.getProcessor because after unlocking the slice may have changed for stacks.
case Layer.IMAGEPLUS:
return imp;
default:
Utils.log("FSLoader.fetchImage: Unknown format " + format);
return null;
}
}
}
}
/** Returns the alpha mask image from a file, or null if none stored. */
@Override
public ByteProcessor fetchImageMask(final Patch p) {
return p.getAlphaMask();
}
@Override
synchronized public final String getMasksFolder() {
if (null == dir_masks) createMasksFolder();
return dir_masks;
}
synchronized private final void createMasksFolder() {
if (null == dir_masks) dir_masks = getUNUIdFolder() + "trakem2.masks/";
final File f = new File(dir_masks);
if (f.exists() && f.isDirectory()) return;
try {
f.mkdirs();
} catch (Exception e) {
IJError.print(e);
}
}
private String dir_cts = null;
@Override
synchronized public final String getCoordinateTransformsFolder() {
if (null == dir_cts) createCoordinateTransformsFolder();
return dir_cts;
}
synchronized private final void createCoordinateTransformsFolder() {
if (null == dir_cts) dir_cts = getUNUIdFolder() + "trakem2.cts/";
final File f = new File(dir_cts);
if (f.exists() && f.isDirectory()) return;
try {
f.mkdirs();
} catch (Exception e) {
IJError.print(e);
}
}
/** Loaded in full from XML file */
public Object[] fetchLabel(DLabel label) {
return null;
}
/** Loads and returns the original image, which is not cached, or returns null if it's not different than the working image. */
synchronized public ImagePlus fetchOriginal(final Patch patch) {
String original_path = patch.getOriginalPath();
if (null == original_path) return null;
// else, reserve memory and open it:
releaseToFit(estimateImageFileSize(patch, 0));
try {
return openImage(original_path);
} catch (Throwable t) {
IJError.print(t);
}
return null;
}
/* GENERIC, from DBObject calls. Records the id of the object in the HashMap ht_dbo.
* Always returns true. Does not check if another object has the same id.
*/
public boolean addToDatabase(final DBObject ob) {
synchronized (db_lock) {
setChanged(true);
final long id = ob.getId();
if (id > max_id) {
max_id = id;
}
if (ob.getClass() == Patch.class) {
final Patch p = (Patch)ob;
if (p.hasCoordinateTransform()) {
max_blob_id = Math.max(p.getCoordinateTransformId(), max_blob_id);
}
if (p.hasAlphaMask()) {
max_blob_id = Math.max(p.getAlphaMaskId(), max_blob_id);
}
}
}
return true;
}
public boolean updateInDatabase(final DBObject ob, final String key) {
// Should only be GUI-driven
setChanged(true);
//
if (ob.getClass() == Patch.class) {
Patch p = (Patch)ob;
if (key.equals("tiff_working")) return null != setImageFile(p, fetchImagePlus(p));
}
return true;
}
public boolean updateInDatabase(final DBObject ob, final Set<String> keys) {
// Should only be GUI-driven
setChanged(true);
if (ob.getClass() == Patch.class) {
Patch p = (Patch)ob;
if (keys.contains("tiff_working")) return null != setImageFile(p, fetchImagePlus(p));
}
return true;
}
public boolean removeFromDatabase(final DBObject ob) {
synchronized (db_lock) {
setChanged(true);
// remove from the hashtable
final long loid = ob.getId();
Utils.log2("removing " + Project.getName(ob.getClass()) + " " + ob);
if (ob.getClass() == Patch.class) {
try {
// STRATEGY change: images are not owned by the FSLoader.
Patch p = (Patch)ob;
if (!ob.getProject().getBooleanProperty("keep_mipmaps")) removeMipMaps(p);
ht_paths.remove(p.getId()); // after removeMipMaps !
mawts.remove(loid);
cannot_regenerate.remove(p);
flushMipMaps(p.getId()); // locks on its own
touched_mipmaps.remove(p);
return true;
} catch (Throwable t) {
handleCacheError(t);
}
}
}
return true;
}
/** 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.
* If the Patch p current image path is different than its original image path, then the file is overwritten if it exists already.
*/
public String setImageFile(final Patch p, final ImagePlus imp) {
if (null == imp) return null;
try {
String path = getAbsolutePath(p);
String slice = null;
//
// path can be null if the image is pasted, or from a copy, or totally new
if (null != path) {
int i_sl = path.lastIndexOf("-----#slice=");
if (-1 != i_sl) {
slice = path.substring(i_sl);
path = path.substring(0, i_sl);
}
} else {
// no path, inspect image FileInfo's path if the image has no changes
if (!imp.changes) {
final FileInfo fi = imp.getOriginalFileInfo();
if (null != fi && null != fi.directory && null != fi.fileName) {
final String fipath = fi.directory.replace('\\', '/') + "/" + fi.fileName;
if (new File(fipath).exists()) {
// no need to save a new image, it exists and has no changes
updatePaths(p, fipath, null != slice);
cacheAll(p, imp);
Utils.log2("Reusing image file: path exists for fileinfo at " + fipath);
return fipath;
}
}
}
}
if (null != path) {
final String starting_path = path;
// Save as a separate image in a new path within the storage folder
String filename = path.substring(path.lastIndexOf('/') +1);
//Utils.log2("filename 1: " + filename);
// remove .tif extension if there
if (filename.endsWith(".tif")) filename = filename.substring(0, filename.length() -3); // keep the dot
//Utils.log2("filename 2: " + filename);
// check if file ends with a tag of form ".id1234." where 1234 is p.getId()
final String tag = ".id" + p.getId() + ".";
if (!filename.endsWith(tag)) filename += tag.substring(1); // without the starting dot, since it has one already
// reappend extension
filename += "tif";
//Utils.log2("filename 3: " + filename);
path = getImageStorageFolder() + filename;
if (path.equals(p.getOriginalPath())) {
// Houston, we have a problem: a user reused a non-original image
File file = null;
int i = 1;
final int itag = path.lastIndexOf(tag);
do {
path = path.substring(0, itag) + "." + i + tag + "tif";
i++;
file = new File(path);
} while (file.exists());
}
//Utils.log2("path to use: " + path);
final String path2 = super.exportImage(p, imp, path, true);
//Utils.log2("path exported to: " + path2);
// update paths' hashtable
if (null != path2) {
updatePaths(p, path2, null != slice);
cacheAll(p, imp);
hs_unloadable.remove(p);
return path2;
} else {
Utils.log("WARNING could not save image at " + path);
// undo
updatePaths(p, starting_path, null != slice);
return null;
}
}
} catch (Exception e) {
IJError.print(e);
}
return null;
}
/** Associate patch with imp, and all slices as well if any. */
private void cacheAll(final Patch p, final ImagePlus imp) {
if (p.isStack()) {
for (Patch pa : p.getStackPatches()) {
cache(pa, imp);
}
} else {
cache(p, imp);
}
}
/** For the Patch and for any associated slices if the patch is part of a stack. */
private void updatePaths(final Patch patch, final String new_path, final boolean is_stack) {
synchronized (db_lock) {
try {
// ensure the old path is cached in the Patch, to get set as the original if there is no original.
String old_path = getAbsolutePath(patch);
if (is_stack) {
old_path = old_path.substring(0, old_path.lastIndexOf("-----#slice"));
for (Patch p : patch.getStackPatches()) {
long pid = p.getId();
String str = ht_paths.get(pid);
int isl = str.lastIndexOf("-----#slice=");
updatePatchPath(p, new_path + str.substring(isl));
}
} else {
Utils.log2("path to set: " + new_path);
Utils.log2("path before: " + ht_paths.get(patch.getId()));
updatePatchPath(patch, new_path);
Utils.log2("path after: " + ht_paths.get(patch.getId()));
}
mawts.updateImagePlusPath(old_path, new_path);
} catch (Throwable e) {
IJError.print(e);
}
}
}
/** With slice info appended at the end; only if it exists, otherwise null. */
public String getAbsolutePath(final Patch patch) {
synchronized (patch) {
String abs_path = patch.getCurrentPath();
if (null != abs_path) return abs_path;
// else, compute, set and return it:
String path = ht_paths.get(patch.getId());
if (null == path) return null;
// substract slice info if there
int i_sl = path.lastIndexOf("-----#slice=");
String slice = null;
if (-1 != i_sl) {
slice = path.substring(i_sl);
path = path.substring(0, i_sl);
}
path = getAbsolutePath(path);
if (null == path) {
Utils.log("Path for patch " + patch + " does not exist: " + path);
return null;
}
// Else assume that it exists.
// reappend slice info if existent
if (null != slice) path += slice;
// set it
patch.cacheCurrentPath(path);
return path;
}
}
/** Return an absolute path made from path: if it's already absolute, retursn itself; otherwise, the parent folder of all relative paths of this Loader is prepended. */
public String getAbsolutePath(String path) {
if (isRelativePath(path)) {
// path is relative: preprend the parent folder of the xml file
path = getParentFolder() + path;
if (!isURL(path) && !new File(path).exists()) {
return null;
}
}
return path;
}
public final String getImageFilePath(final Patch p) {
final String path = getAbsolutePath(p);
if (null == path) return null;
final int i = path.lastIndexOf("-----#slice");
return -1 == i ? path
: path.substring(0, i);
}
public static final boolean isURL(final String path) {
return null != path && 0 == path.indexOf("http://");
}
static public final Pattern ABS_PATH = Pattern.compile("^[a-zA-Z]*:/.*$|^/.*$|[a-zA-Z]:.*$");
public static final boolean isRelativePath(final String path) {
return ! ABS_PATH.matcher(path).matches();
}
/** All backslashes are converted to slashes to avoid havoc in MSWindows. */
public void addedPatchFrom(String path, final Patch patch) {
if (null == path) {
Utils.log("Null path for patch: " + patch);
return;
}
updatePatchPath(patch, path);
}
/** This method has the exclusivity in calling ht_paths.put, because it ensures the path won't have escape characters. */
private final void updatePatchPath(final Patch patch, String path) { // reversed order in purpose, relative to addedPatchFrom
// avoid W1nd0ws nightmares
path = path.replace('\\', '/'); // replacing with chars, in place
// remove double slashes that a user may have slipped in
final int start = isURL(path) ? 6 : (IJ.isWindows() ? 3 : 1);
while (-1 != path.indexOf("//", start)) {
// avoid the potential C:// of windows and the starting // of a samba network
path = path.substring(0, start) + path.substring(start).replace("//", "/");
}
// cache path as absolute
patch.cacheCurrentPath(isRelativePath(path) ? getParentFolder() + path : path);
// if path is absolute, try to make it relative
//Utils.log2("path was: " + path);
path = makeRelativePath(path);
// store
ht_paths.put(patch.getId(), path);
//Utils.log2("Updated patch path " + ht_paths.get(patch.getId()) + " for patch " + patch);
}
/** Takes a String and returns a copy with the following conversions: / to -, space to _, and \ to -. */
static public String asSafePath(final String name) {
return name.trim().replace('/', '-').replace(' ', '_').replace('\\','-');
}
/** 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. */
@Override
public String save(final Project project, XMLOptions options) {
String result = null;
if (null == project_file_path) {
String xml_path = super.saveAs(project, null, options);
if (null == xml_path) return null;
else {
this.project_file_path = xml_path;
ControlWindow.updateTitle(project);
result = this.project_file_path;
}
} else {
File fxml = new File(project_file_path);
result = super.export(project, fxml, options);
}
if (null != result) {
Utils.logAll(Utils.now() + " Saved " + project);
touched_mipmaps.clear();
}
return result;
}
/** The saveAs called from menus via saveTask. */
@Override
public String saveAs(Project project, XMLOptions options) {
String path = super.saveAs(project, null, options);
if (null != path) {
// update the xml path to point to the new one
this.project_file_path = path;
Utils.log2("After saveAs, new xml path is: " + path);
touched_mipmaps.clear();
}
ControlWindow.updateTitle(project);
Display.updateTitle(project);
return path;
}
/** Meant for programmatic access, such as calls to project.saveAs(path, overwrite) which call exactly this method. */
@Override
public String saveAs(final String path, final XMLOptions options) {
if (null == path) {
Utils.log("Cannot save on null path.");
return null;
}
String path2 = path;
String extension = ".xml";
if (path2.endsWith(extension)) {} // all fine
else if (path2.endsWith(".xml.gz")) extension = ".xml.gz";
else {
// neither matches, add the default ".xml"
path2 += extension;
}
File fxml = new File(path2);
if (!fxml.canWrite()) {
// write to storage folder instead
String path3 = path2;
path2 = getStorageFolder() + fxml.getName();
Utils.logAll("WARNING can't write to " + path3 + "\n --> will write instead to " + path2);
fxml = new File(path2);
}
if (!options.overwriteXMLFile) {
int i = 1;
while (fxml.exists()) {
String parent = fxml.getParent().replace('\\','/');
if (!parent.endsWith("/")) parent += "/";
String name = fxml.getName();
name = name.substring(0, name.length() - 4);
path2 = parent + name + "-" + i + extension;
fxml = new File(path2);
i++;
}
}
Project project = Project.findProject(this);
path2 = super.saveAs(project, path2, options);
if (null != path2) {
project_file_path = path2;
Utils.logAll("After saveAs, new xml path is: " + path2);
ControlWindow.updateTitle(project);
touched_mipmaps.clear();
}
return path2;
}
/** Returns the stored path for the given Patch image, which may be relative and may contain slice information appended.*/
public String getPath(final Patch patch) {
return ht_paths.get(patch.getId());
}
protected Map<Long,String> getPathsCopy() {
synchronized (ht_paths) {
return Collections.synchronizedMap(new HashMap<Long,String>(ht_paths));
}
}
/** Try to make all paths in ht_paths be relative to the given xml_path.
* This is intended for making all paths relative when saving to XML for the first time.
* {@code dir_storage} and {@code dir_mipmaps} remain untouched--otherwise,
* after a {@code saveAs}, images would not be found. */
protected void makeAllPathsRelativeTo(final String xml_path, final Project project) {
synchronized (db_lock) {
try {
for (final Map.Entry<Long,String> e : ht_paths.entrySet()) {
e.setValue(FSLoader.makeRelativePath(xml_path, e.getValue()));
}
for (final Stack st : project.getRootLayerSet().getAll(Stack.class)) {
String path = st.getFilePath();
if (!isRelativePath(path)) {
String path2 = makeRelativePath(st.getFilePath());
if (path.equals(path2)) continue; // could not be made relative
else st.setFilePath(path2); // will also flush the cache, so use only if necessary
}
}
} catch (Throwable t) {
IJError.print(t);
}
}
}
protected void restorePaths(final Map<Long,String> copy, final String mipmaps_folder, final String storage_folder) {
synchronized (db_lock) {
try {
this.dir_mipmaps = mipmaps_folder;
this.dir_storage = storage_folder;
ht_paths.clear();
ht_paths.putAll(copy);
} catch (Throwable t) {
IJError.print(t);
}
}
}
/** 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. */
public String makeRelativePath(String path) {
return FSLoader.makeRelativePath(this.project_file_path, path);
}
static private String makeRelativePath(final String project_file_path, String path) {
if (null == project_file_path) {
//unsaved project
return path;
}
if (null == path) {
return null;
}
// fix W1nd0ws paths
path = path.replace('\\', '/'); // char-based, no parsing problems
// remove slice tag
String slice = null;
int isl = path.lastIndexOf("-----#slice");
if (-1 != isl) {
slice = path.substring(isl);
path = path.substring(0, isl);
}
//
if (FSLoader.isRelativePath(path)) {
// already relative
if (-1 != isl) path += slice;
return path;
}
// the long and verbose way, to be cross-platform. Should work with URLs just the same.
String xdir = new File(project_file_path).getParentFile().getAbsolutePath();
if (IJ.isWindows()) {
xdir = xdir.replace('\\', '/');
path = path.replace('\\', '/');
}
if (!xdir.endsWith("/")) xdir += "/";
if (path.startsWith(xdir)) {
path = path.substring(xdir.length());
}
if (-1 != isl) path += slice;
//Utils.log("made relative path: " + path);
return path;
}
/** Adds a "Save" and "Save as" menu items. */
public void setupMenuItems(final JMenu menu, final Project project) {
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
saveTask(project, ae.getActionCommand());
}
};
JMenuItem item;
item = new JMenuItem("Save"); item.addActionListener(listener); menu.add(item);
item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true));
item = new JMenuItem("Save as..."); item.addActionListener(listener); menu.add(item);
final JMenu adv = new JMenu("Advanced");
item = new JMenuItem("Save as... without coordinate transforms"); item.addActionListener(listener); adv.add(item);
item = new JMenuItem("Delete stale files..."); item.addActionListener(listener); adv.add(item);
menu.add(adv);
menu.addSeparator();
}
/** Returns the last Patch. */
protected Patch importStackAsPatches(final Project project, final Layer first_layer, final double x, final double y, final ImagePlus imp_stack, final boolean as_copy, final String filepath) {
Utils.log2("FSLoader.importStackAsPatches filepath=" + filepath);
String target_dir = null;
if (as_copy) {
DirectoryChooser dc = new DirectoryChooser("Folder to save images");
target_dir = dc.getDirectory();
if (null == target_dir) return null; // user canceled dialog
if (IJ.isWindows()) target_dir = target_dir.replace('\\', '/');
if (target_dir.length() -1 != target_dir.lastIndexOf('/')) {
target_dir += "/";
}
}
// Double.MAX_VALUE is a flag to indicate "add centered"
double pos_x = Double.MAX_VALUE != x ? x : first_layer.getLayerWidth()/2 - imp_stack.getWidth()/2;
double pos_y = Double.MAX_VALUE != y ? y : first_layer.getLayerHeight()/2 - imp_stack.getHeight()/2;
final double thickness = first_layer.getThickness();
final String title = Utils.removeExtension(imp_stack.getTitle()).replace(' ', '_');
Utils.showProgress(0);
Patch previous_patch = null;
final int n = imp_stack.getStackSize();
final ImageStack stack = imp_stack.getStack();
final boolean virtual = stack.isVirtual();
final VirtualStack vs = virtual ? (VirtualStack)stack : null;
for (int i=1; i<=n; i++) {
Layer layer = first_layer;
double z = first_layer.getZ() + (i-1) * thickness;
if (i > 1) layer = first_layer.getParent().getLayer(z, thickness, true); // will create new layer if not found
if (null == layer) {
Utils.log("Display.importStack: could not create new layers.");
return null;
}
String patch_path = null;
ImagePlus imp_patch_i = null;
if (virtual) { // because we love inefficiency, every time all this is done again
//VirtualStack vs = (VirtualStack)imp_stack.getStack();
String vs_dir = vs.getDirectory().replace('\\', '/');
if (!vs_dir.endsWith("/")) vs_dir += "/";
String iname = vs.getFileName(i);
patch_path = vs_dir + iname;
Utils.log2("virtual stack: patch path is " + patch_path);
releaseToFit(new File(patch_path).length() * 3);
Utils.log2(i + " : " + patch_path);
imp_patch_i = openImage(patch_path);
} else {
ImageProcessor ip = stack.getProcessor(i);
if (as_copy) ip = ip.duplicate();
imp_patch_i = new ImagePlus(title + "__slice=" + i, ip);
}
String label = stack.getSliceLabel(i);
if (null == label) label = "";
Patch patch = null;
if (as_copy) {
patch_path = target_dir + cleanSlashes(imp_patch_i.getTitle()) + ".zip";
ini.trakem2.io.ImageSaver.saveAsZip(imp_patch_i, patch_path);
patch = new Patch(project, label + " " + title + " " + i, pos_x, pos_y, imp_patch_i);
} else if (virtual) {
patch = new Patch(project, label, pos_x, pos_y, imp_patch_i);
} else {
patch_path = filepath + "-----#slice=" + i;
//Utils.log2("path is "+ patch_path);
final AffineTransform atp = new AffineTransform();
atp.translate(pos_x, pos_y);
patch = new Patch(project, getNextId(), label + " " + title + " " + i, imp_stack.getWidth(), imp_stack.getHeight(), imp_stack.getWidth(), imp_stack.getHeight(), imp_stack.getType(), false, imp_stack.getProcessor().getMin(), imp_stack.getProcessor().getMax(), atp);
patch.addToDatabase();
//Utils.log2("type is " + imp_stack.getType());
}
Utils.log2("B: " + i + " : " + patch_path);
addedPatchFrom(patch_path, patch);
if (!as_copy && !virtual) {
if (virtual) cache(patch, imp_patch_i); // each slice separately
else cache(patch, imp_stack); // uses the entire stack, shared among all Patch instances
}
if (isMipMapsRegenerationEnabled()) regenerateMipMaps(patch); // submit for regeneration
if (null != previous_patch) patch.link(previous_patch);
layer.add(patch);
previous_patch = patch;
Utils.showProgress(i * (1.0 / n));
}
Utils.showProgress(1.0);
// update calibration
// TODO
// return the last patch
return previous_patch;
}
/** Replace forward slashes and backslashes with hyphens. */
private final String cleanSlashes(final String s) {
return s.replace('\\', '-').replace('/', '-');
}
/** Specific options for the Loader which exist as attributes to the Project XML node. */
public void parseXMLOptions(final HashMap<String,String> ht_attributes) {
// Adding some logic to support old projects which lack a storage folder and a mipmaps folder
// and also to prevent errors such as those created when manualy tinkering with the XML file
// or renaming directories, etc.
String ob = ht_attributes.remove("storage_folder");
if (null != ob) {
String sf = ob.replace('\\', '/');
if (isRelativePath(sf)) {
sf = getParentFolder() + sf;
}
if (isURL(sf)) {
// can't be an URL
Utils.log2("Can't have an URL as the path of a storage folder.");
} else {
File f = new File(sf);
if (f.exists() && f.isDirectory()) {
this.dir_storage = sf;
} else {
Utils.log2("storage_folder was not found or is invalid: " + ob);
}
}
}
if (null == this.dir_storage) {
// select the directory where the xml file lives.
this.dir_storage = getParentFolder();
if (null == this.dir_storage || isURL(this.dir_storage)) this.dir_storage = null;
if (null == this.dir_storage && ControlWindow.isGUIEnabled()) {
Utils.log2("Asking user for a storage folder in a dialog."); // tip for headless runners whose program gets "stuck"
DirectoryChooser dc = new DirectoryChooser("REQUIRED: select a storage folder");
this.dir_storage = dc.getDirectory();
}
if (null == this.dir_storage) {
IJ.showMessage("TrakEM2 requires a storage folder.\nTemporarily your home directory will be used.");
this.dir_storage = System.getProperty("user.home");
}
}
// fix
if (null != this.dir_storage) {
if (IJ.isWindows()) this.dir_storage = this.dir_storage.replace('\\', '/');
if (!this.dir_storage.endsWith("/")) this.dir_storage += "/";
}
Utils.log2("storage folder is " + this.dir_storage);
//
ob = ht_attributes.remove("mipmaps_folder");
if (null != ob) {
String mf = ob.replace('\\', '/');
if (isRelativePath(mf)) {
mf = getParentFolder() + mf;
}
if (isURL(mf)) {
this.dir_mipmaps = mf;
// TODO must disable input somehow, so that images are not edited.
} else {
File f = new File(mf);
if (f.exists() && f.isDirectory()) {
this.dir_mipmaps = mf;
} else {
Utils.log2("mipmaps_folder was not found or is invalid: " + ob);
}
}
}
ob = ht_attributes.remove("mipmaps_regen");
if (null != ob) {
this.mipmaps_regen = Boolean.parseBoolean(ob);
}
ob = ht_attributes.get("n_mipmap_threads");
if (null != ob) {
int n_threads = Math.max(1, Integer.parseInt(ob));
FSLoader.restartMipMapThreads(n_threads);
}
// parse the unuid before attempting to create any folders
this.unuid = ht_attributes.remove("unuid");
// Attempt to get an existing UNUId folder, for .xml files that share the same mipmaps folder
if (ControlWindow.isGUIEnabled() && null == this.unuid) {
obtainUNUIdFolder();
}
if (null == this.dir_mipmaps) {
// create a new one inside the dir_storage, which can't be null
createMipMapsDir(dir_storage);
if (null != this.dir_mipmaps && ControlWindow.isGUIEnabled() && null != IJ.getInstance()) {
notifyMipMapsOutOfSynch();
}
}
// fix
if (null != this.dir_mipmaps && !this.dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
Utils.log2("mipmaps folder is " + this.dir_mipmaps);
if (null == unuid) {
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.");
Utils.log2("Creating unuid for project " + this);
this.unuid = createUNUId(dir_storage);
fixStorageFolders();
Utils.log2("Now mipmaps folder is " + this.dir_mipmaps);
if (null != dir_masks) Utils.log2("Now masks folder is " + this.dir_masks);
}
final String s_mipmaps_format = (String) ht_attributes.remove("mipmaps_format");
if (null != s_mipmaps_format) {
final int mipmaps_format = Integer.parseInt(s_mipmaps_format.trim());
if (mipmaps_format >= 0 && mipmaps_format < MIPMAP_FORMATS.length) {
Utils.log2("Set mipmap format to " + mipmaps_format);
setMipMapFormat(mipmaps_format);
}
}
}
private void notifyMipMapsOutOfSynch() {
Utils.log2("'ok' dialog to explain that mipmaps may be in disagreement with the XML file."); // tip for headless runners whose program gets "stuck"
Utils.showMessage("TrakEM2 detected a crash", "TrakEM2 detected a crash. Image mipmap files may be out of synch.\n\nIf you where editing images when the crash occurred,\nplease right-click and run 'Project - Regenerate all mipmaps'");
}
/** Order the regeneration of all mipmaps for the Patch instances in {@code patches}, setting up a task that blocks input until all completed. */
public Bureaucrat regenerateMipMaps(final Collection<? extends Displayable> patches) {
return Bureaucrat.createAndStart(new Worker.Task("Regenerating mipmaps") { public void exec() {
final List<Future<?>> fus = new ArrayList<Future<?>>();
for (final Displayable d : patches) {
if (d.getClass() != Patch.class) continue;
fus.add(d.getProject().getLoader().regenerateMipMaps((Patch) d));
}
// Wait until all done
for (final Future<?> fu : fus) try {
if (null != fu) fu.get(); // fu could be null if a task was not submitted because it's already being done or it failed in some way.
} catch (Exception e) { IJError.print(e); }
}}, Project.findProject(this));
}
/** Specific options for the Loader which exist as attributes to the Project XML node. */
@Override
public void insertXMLOptions(final StringBuilder sb_body, final String indent) {
sb_body.append(indent).append("unuid=\"").append(unuid).append("\"\n");
if (null != dir_mipmaps) sb_body.append(indent).append("mipmaps_folder=\"").append(makeRelativePath(dir_mipmaps)).append("\"\n");
if (null != dir_storage) sb_body.append(indent).append("storage_folder=\"").append(makeRelativePath(dir_storage)).append("\"\n");
sb_body.append(indent).append("mipmaps_format=\"").append(mipmaps_format).append("\"\n");
}
/** Return the path to the folder containing the project XML file. */
public final String getParentFolder() {
return this.project_file_path.substring(0, this.project_file_path.lastIndexOf('/')+1);
}
/* ************** MIPMAPS **********************/
/** Returns the path to the directory hosting the file image pyramids. */
public String getMipMapsFolder() {
return dir_mipmaps;
}
/*
static private IndexColorModel thresh_cm = null;
static private final IndexColorModel getThresholdLUT() {
if (null == thresh_cm) {
// An array of all black pixels (value 0) except at 255, which is white (value 255).
final byte[] c = new byte[256];
c[255] = (byte)255;
thresh_cm = new IndexColorModel(8, 256, c, c, c);
}
return thresh_cm;
}
*/
/** Returns the array of pixels, whose type depends on the bi.getType(); for example, for a BufferedImage.TYPE_BYTE_INDEXED, returns a byte[]. */
static public final Object grabPixels(final BufferedImage bi) {
final PixelGrabber pg = new PixelGrabber(bi, 0, 0, bi.getWidth(), bi.getHeight(), false);
try {
pg.grabPixels();
return pg.getPixels();
} catch (InterruptedException e) {
IJError.print(e);
}
return null;
}
private final BufferedImage createCroppedAlpha(final BufferedImage alpha, final BufferedImage outside) {
if (null == outside) return alpha;
final int width = outside.getWidth();
final int height = outside.getHeight();
// Create an outside image, thresholded: only pixels of 255 remain as 255, the rest is set to 0.
/* // DOESN'T work: creates a mask with "black" as 254 (???), and white 255 (correct).
final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, getThresholdLUT());
thresholded.createGraphics().drawImage(outside, 0, 0, null);
*/
// So, instead: grab the pixels, fix them manually
// The cast to byte[] works because "outside" and "alpha" are TYPE_BYTE_INDEXED.
final byte[] o = (byte[])grabPixels(outside);
if (null == o) return null;
final byte[] a = null == alpha ? o : (byte[])grabPixels(alpha);
// Set each non-255 pixel in outside to 0 in alpha:
for (int i=0; i<o.length; i++) {
if ( (o[i]&0xff) < 255) a[i] = 0;
}
// Put the pixels back into an image:
final BufferedImage thresholded = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, Loader.GRAY_LUT);
thresholded.getRaster().setDataElements(0, 0, width, height, a);
return thresholded;
}
/** WARNING will resize the FloatProcessorT2 source in place, unlike ImageJ standard FloatProcessor class. */
static final private byte[] gaussianBlurResizeInHalf(final FloatProcessorT2 source)
{
new GaussianBlur().blurFloat( source, SIGMA_2, SIGMA_2, 0.01 );
source.halfSizeInPlace();
return (byte[])source.convertToByte(false).getPixels(); // no scaling
}
/** Queue/unqueue for mipmap removal on shutdown without saving;
* the {@code yes}, when true, makes the {@code p} be queued,
* and when false, be removed from the queue. */
public void queueForMipmapRemoval(final Patch p, boolean yes) {
if (yes) touched_mipmaps.add(p);
else touched_mipmaps.remove(p);
}
/** Queue/unqueue for mipmap removal on shutdown without saving;
* the {@code yes}, when true, makes the {@code p} be queued,
* and when false, be removed from the queue. */
public void tagForMipmapRemoval(final Patch p, final boolean yes) {
if (yes) mipmaps_to_remove.add(p);
else mipmaps_to_remove.remove(p);
}
/** Given an image and its source file name (without directory prepended), generate
* a pyramid of images until reaching an image not smaller than 32x32 pixels.
* <p>
* Such images are stored as jpeg 85% quality in a folder named trakem2.mipmaps.
* </p>
* <p>
* The Patch id and the right extension will be appended to the filename in all cases.
* </p>
* <p>
* Any equally named files will be overwritten.
* </p>
*/
protected boolean generateMipMaps(final Patch patch) {
Utils.log2("mipmaps for " + patch);
final String path = getAbsolutePath(patch);
if (null == path) {
Utils.log("generateMipMaps: null path for Patch " + patch);
cannot_regenerate.add(patch);
return false;
}
if (hs_unloadable.contains(patch)) {
FilePathRepair.add(patch);
return false;
}
synchronized (gm_lock) {
try {
if (null == dir_mipmaps) createMipMapsDir(null);
if (null == dir_mipmaps || isURL(dir_mipmaps)) return false;
} catch (Exception e) {
IJError.print(e);
}
}
/** Record Patch as modified */
touched_mipmaps.add(patch);
/** Remove serialized features, if any */
removeSerializedFeatures(patch);
/** Remove serialized pointmatches, if any */
removeSerializedPointMatches(patch);
/** Alpha mask: setup to check if it was modified while regenerating. */
final long alpha_mask_id = patch.getAlphaMaskId();
final int resizing_mode = patch.getProject().getMipMapsMode();
try {
ImageProcessor ip;
ByteProcessor alpha_mask = null;
ByteProcessor outside_mask = null;
int type = patch.getType();
// Aggressive cache freeing
releaseToFit(patch.getOWidth() * patch.getOHeight() * 4 + MIN_FREE_BYTES);
// Obtain an image which may be coordinate-transformed, and an alpha mask.
Patch.PatchImage pai = patch.createTransformedImage();
if (null == pai || null == pai.target) {
Utils.log("Can't regenerate mipmaps for patch " + patch);
cannot_regenerate.add(patch);
return false;
}
ip = pai.target;
alpha_mask = pai.mask; // can be null
outside_mask = pai.outside; // can be null
pai = null;
// Old style:
//final String filename = new StringBuilder(new File(path).getName()).append('.').append(patch.getId()).append(mExt).toString();
// New style:
final String filename = createMipMapRelPath(patch, mExt);
int w = ip.getWidth();
int h = ip.getHeight();
// sigma = sqrt(2^level - 0.5^2)
// where 0.5 is the estimated sigma for a full-scale image
// which means sigma = 0.75 for the full-scale image (has level 0)
// prepare a 0.75 sigma image from the original
double min = patch.getMin(),
max = patch.getMax();
// Fix improper min,max values
// (The -1,-1 are flags really for "not set")
if (-1 == min && -1 == max) {
switch (type) {
case ImagePlus.COLOR_RGB:
case ImagePlus.COLOR_256:
case ImagePlus.GRAY8:
patch.setMinAndMax(0, 255);
break;
// Find and flow through to default:
case ImagePlus.GRAY16:
((ij.process.ShortProcessor)ip).findMinAndMax();
patch.setMinAndMax(ip.getMin(), ip.getMax());
break;
case ImagePlus.GRAY32:
((FloatProcessor)ip).findMinAndMax();
patch.setMinAndMax(ip.getMin(), ip.getMax());
break;
}
min = patch.getMin(); // may have changed
max = patch.getMax();
}
// Set for the level 0 image, which is a duplicate of the one in the cache in any case
ip.setMinAndMax(min, max);
// ImageJ no longer stretches the bytes for ByteProcessor with setMinAndmax
if (ByteProcessor.class == ip.getClass()) {
if (0 != min && 255 != max) {
final byte[] b = (byte[]) ip.getPixels();
final double scale = 255 / (max - min);
for (int i=0; i<b.length; ++i) {
final int val = b[i] & 0xff;
if (val < min) b[i] = 0;
else b[i] = (byte)Math.min(255, ((val - min) * scale));
}
}
}
// Proper support for LUT images: treat them as RGB
if (ip.isColorLut() || type == ImagePlus.COLOR_256) {
ip = ip.convertToRGB();
type = ImagePlus.COLOR_RGB;
}
if (Loader.AREA_DOWNSAMPLING == resizing_mode) {
long t0 = System.currentTimeMillis();
final ImageBytes[] b = DownsamplerMipMaps.create(patch, type, ip, alpha_mask, outside_mask);
long t1 = System.currentTimeMillis();
for (int i=0; i<b.length; ++i) {
mmio.save(getLevelDir(dir_mipmaps, i) + filename, b[i].c, b[i].width, b[i].height, 0.85f);
}
long t2 = System.currentTimeMillis();
System.out.println("MipMaps with area downsampling: creation took " + (t1 - t0) + "ms, saving took " + (t2 - t1) + "ms, total: " + (t2 - t0) + "ms\n");
} else if (Loader.GAUSSIAN == resizing_mode) {
if (ImagePlus.COLOR_RGB == type) {
// TODO releaseToFit proper
releaseToFit(w * h * 4 * 10);
final ColorProcessor cp = (ColorProcessor)ip;
final FloatProcessorT2 red = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(0, red);
final FloatProcessorT2 green = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(1, green);
final FloatProcessorT2 blue = new FloatProcessorT2(w, h, 0, 255); cp.toFloat(2, blue);
FloatProcessorT2 alpha;
final FloatProcessorT2 outside;
if (null != alpha_mask) {
alpha = new FloatProcessorT2(alpha_mask);
} else {
alpha = null;
}
if (null != outside_mask) {
outside = new FloatProcessorT2(outside_mask);
if ( null == alpha ) {
alpha = outside;
alpha_mask = outside_mask;
}
} else {
outside = null;
}
final String target_dir0 = getLevelDir(dir_mipmaps, 0);
if (Thread.currentThread().isInterrupted()) return false;
// Generate level 0 first:
// TODO Add alpha information into the int[] pixel array or make the image visible some other way
if (!(null == alpha ? mmio.save(cp, target_dir0 + filename, 0.85f, false)
: mmio.save(target_dir0 + filename, P.asRGBABytes((int[])cp.getPixels(), (byte[])alpha_mask.getPixels(), null == outside ? null : (byte[])outside_mask.getPixels()), w, h, 0.85f))) {
Utils.log("Failed to save mipmap for COLOR_RGB, 'alpha = " + alpha + "', level = 0 for patch " + patch);
cannot_regenerate.add(patch);
} else {
int k = 0; // the scale level. Proper scale is: 1 / pow(2, k)
do {
if (Thread.currentThread().isInterrupted()) return false;
// 1 - Prepare values for the next scaled image
k++;
// 2 - Check that the target folder for the desired scale exists
final String target_dir = getLevelDir(dir_mipmaps, k);
if (null == target_dir) continue;
// 3 - Blur the previous image to 0.75 sigma, and scale it
final byte[] r = gaussianBlurResizeInHalf(red); // will resize 'red' FloatProcessor in place.
final byte[] g = gaussianBlurResizeInHalf(green); // idem
final byte[] b = gaussianBlurResizeInHalf(blue); // idem
final byte[] a = null == alpha ? null : gaussianBlurResizeInHalf(alpha); // idem
if ( null != outside ) {
final byte[] o;
if (alpha != outside)
o = gaussianBlurResizeInHalf(outside); // idem
else
o = a;
// Remove all not completely inside pixels from the alphamask
// If there was no alpha mask, alpha is the outside itself
for (int i=0; i<o.length; i++) {
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;
}
}
w = red.getWidth();
h = red.getHeight();
// 4 - Compose ColorProcessor
if (null == alpha) {
// 5 - Save as jpeg
if (!mmio.save(target_dir + filename, new byte[][]{r, g, b}, w, h, 0.85f)) {
Utils.log("Failed to save mipmap for COLOR_RGB, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
cannot_regenerate.add(patch);
break;
}
} else {
if (!mmio.save(target_dir + filename, new byte[][]{r, g, b, a}, w, h, 0.85f)) {
Utils.log("Failed to save mipmap for COLOR_RGB, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
cannot_regenerate.add(patch);
break;
}
}
} while (w >= 32 && h >= 32); // not smaller than 32x32
}
} else {
long t0 = System.currentTimeMillis();
// Greyscale:
releaseToFit(w * h * 4 * 10);
if (Thread.currentThread().isInterrupted()) return false;
final FloatProcessorT2 fp = new FloatProcessorT2((FloatProcessor) ip.convertToFloat());
if (ImagePlus.GRAY8 == type) {
// for 8-bit, the min,max has been applied when going to FloatProcessor
fp.setMinMax(0, 255); // just set it
} else {
fp.setMinAndMax(patch.getMin(), patch.getMax());
}
//fp.debugMinMax(patch.toString());
FloatProcessorT2 alpha, outside;
if (null != alpha_mask) {
alpha = new FloatProcessorT2(alpha_mask);
} else {
alpha = null;
}
if (null != outside_mask) {
outside = new FloatProcessorT2(outside_mask);
if (null == alpha) {
alpha = outside;
alpha_mask = outside_mask;
}
} else {
outside = null;
}
int k = 0; // the scale level. Proper scale is: 1 / pow(2, k)
do {
if (Thread.currentThread().isInterrupted()) return false;
if (0 != k) { // not doing so at the end because it would add one unnecessary blurring
gaussianBlurResizeInHalf( fp );
if (null != alpha) {
gaussianBlurResizeInHalf( alpha );
if (alpha != outside && outside != null) {
gaussianBlurResizeInHalf( outside );
}
}
}
w = fp.getWidth();
h = fp.getHeight();
// 1 - check that the target folder for the desired scale exists
final String target_dir = getLevelDir(dir_mipmaps, k);
if (null == target_dir) continue;
if (null != alpha) {
// 3 - save as jpeg with alpha
// Remove all not completely inside pixels from the alpha mask
// If there was no alpha mask, alpha is the outside itself
if (!mmio.save(target_dir + filename, new byte[][]{fp.getScaledBytePixels(), P.merge(alpha.getBytePixels(), null == outside ? null : outside.getBytePixels())}, w, h, 0.85f)) {
Utils.log("Failed to save mipmap for GRAY8, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
cannot_regenerate.add(patch);
break;
}
} else {
// 3 - save as 8-bit jpeg
if (!mmio.save(target_dir + filename, new byte[][]{fp.getScaledBytePixels()}, w, h, 0.85f)) {
Utils.log("Failed to save mipmap for GRAY8, 'alpha = " + alpha + "', level = " + k + " for patch " + patch);
cannot_regenerate.add(patch);
break;
}
}
// 4 - prepare values for the next scaled image
k++;
} while (fp.getWidth() >= 32 && fp.getHeight() >= 32); // not smaller than 32x32
long t1 = System.currentTimeMillis();
System.out.println("MipMaps took " + (t1 - t0));
}
} else {
Utils.log("ERROR: unknown image resizing mode for mipmaps: " + resizing_mode);
}
return true;
} catch (Throwable e) {
Utils.log("*** ERROR: Can't generate mipmaps for patch " + patch);
IJError.print(e);
cannot_regenerate.add(patch);
return false;
} finally {
// flush any cached tiles
flushMipMaps(patch.getId());
// flush any cached layer screenshots
if (null != patch.getLayer()) {
try { patch.getLayer().getParent().removeFromOffscreens(patch.getLayer()); } catch (Exception e) { IJError.print(e); }
}
// gets executed even when returning from the catch statement or within the try/catch block
synchronized (gm_lock) {
regenerating_mipmaps.remove(patch);
}
// Has the alpha mask changed?
if (patch.getAlphaMaskId() != alpha_mask_id) {
Utils.log2("Alpha mask changed: resubmitting mipmap regeneration for " + patch);
regenerateMipMaps(patch);
}
}
}
/** Remove the file, if it exists, with serialized features for patch.
* Returns true when no such file or on success; false otherwise. */
public boolean removeSerializedFeatures(final Patch patch) {
final File f = new File(new StringBuilder(getUNUIdFolder()).append("features.ser/").append(FSLoader.createIdPath(Long.toString(patch.getId()), "features", ".ser")).toString());
if (f.exists()) {
try {
return f.delete();
} catch (Exception e) {
IJError.print(e);
return false;
}
} else return true;
}
/** Remove the file, if it exists, with serialized point matches for patch.
* Returns true when no such file or on success; false otherwise. */
public boolean removeSerializedPointMatches(final Patch patch) {
final String ser = new StringBuilder(getUNUIdFolder()).append("pointmatches.ser/").toString();
final File fser = new File(ser);
if (!fser.exists() || !fser.isDirectory()) return true;
boolean success = true;
final String sid = Long.toString(patch.getId());
final ArrayList<String> removed_paths = new ArrayList<String>();
// 1 - Remove all files with <p1.id>_<p2.id>:
if (sid.length() < 2) {
// Delete all files starting with sid + '_' and present directly under fser
success = Utils.removePrefixedFiles(fser, sid + "_", removed_paths);
} else {
final String sid_ = sid + "_"; // minimal 2 length: a number and the underscore
final int len = sid_.length();
final StringBuilder dd = new StringBuilder();
for (int i=1; i<=len; i++) {
dd.append(sid_.charAt(i-1));
if (0 == i % 2 && len != i) dd.append('/');
}
final String med = dd.toString();
final int last_slash = med.lastIndexOf('/');
final File med_parent = new File(ser + med.substring(0, last_slash+1));
// case of 12/34/_* ---> use prefix: "_"
// case of 12/34/5_/* ---> use prefix: last number plus underscore, aka: med.substring(med.length()-2);
success = Utils.removePrefixedFiles(med_parent,
last_slash == med.length() -2 ? "_" : med.substring(med.length() -2),
removed_paths);
}
// 2 - For each removed path, find the complementary: <*>_<p1.id>
for (String path : removed_paths) {
if (IJ.isWindows()) path = path.replace('\\', '/');
File f = new File(path);
// Check that its a pointmatches file
int idot = path.lastIndexOf(".pointmatches.ser");
if (idot < 0) {
Utils.log2("Not a pointmatches.ser file: can't process " + path);
continue;
}
// Find the root
int ifolder = path.indexOf("pointmatches.ser/");
if (ifolder < 0) {
Utils.log2("Not in pointmatches.ser/ folder:" + path);
continue;
}
String dir = path.substring(0, ifolder + 17);
// Cut the beginning and the end
String name = path.substring(dir.length(), idot);
Utils.log2("name: " + name);
// Remove all path separators
name = name.replaceAll("/", "");
int iunderscore = name.indexOf('_');
if (-1 == iunderscore) {
Utils.log2("No underscore: can't process " + path);
continue;
}
name = FSLoader.createIdPath(new StringBuilder().append(name.substring(iunderscore+1)).append('_').append(name.substring(0, iunderscore)).toString(), "pointmatches", ".ser");
f = new File(dir + name);
if (f.exists()) {
if (!f.delete()) {
Utils.log2("Could not delete " + f.getAbsolutePath());
success = false;
} else {
Utils.log2("Deleted pointmatches file " + name);
// Now remove its parent directories within pointmatches.ser/ directory, if they are empty
int islash = name.lastIndexOf('/');
String dirname = name;
while (islash > -1) {
dirname = dirname.substring(0, islash);
if (!Utils.removeFile(new File(dir + dirname))) {
// directory not empty
break;
}
islash = dirname.lastIndexOf('/');
}
}
} else {
Utils.log2("File does not exist: " + dir + name);
}
}
return success;
}
/** 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.
*
* @param patches : the list of Patch instances to generate mipmaps for.
* @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.)
* */
public Bureaucrat generateMipMaps(final Collection<Displayable> patches, final boolean overwrite) {
if (null == patches || 0 == patches.size()) return null;
if (null == dir_mipmaps) createMipMapsDir(null);
if (isURL(dir_mipmaps)) {
Utils.log("Mipmaps folder is an URL, can't save files into it.");
return null;
}
return Bureaucrat.createAndStart(new Worker.Task("Generating MipMaps") {
public void exec() {
this.setAsBackground(true);
Utils.log2("starting mipmap generation ..");
try {
final ArrayList<Future<?>> fus = new ArrayList<Future<?>>();
for (final Displayable displ : patches) {
if (displ.getClass() != Patch.class) continue;
Patch pa = (Patch)displ;
boolean ow = overwrite;
if (!overwrite) {
// check if all the files exist. If one doesn't, then overwrite all anyway
int w = (int)pa.getWidth();
int h = (int)pa.getHeight();
int level = 0;
final String filename = new File(getAbsolutePath(pa)).getName() + "." + pa.getId() + mExt;
do {
w /= 2;
h /= 2;
level++;
if (!new File(dir_mipmaps + level + "/" + filename).exists()) {
ow = true;
break;
}
} while (w >= 32 && h >= 32);
}
if (!ow) continue;
fus.add(regenerateMipMaps(pa));
}
Utils.wait(fus);
} catch (Exception e) {
IJError.print(e);
}
}
}, ((Displayable)patches.iterator().next()).getProject());
}
static private final Object FSLOCK = new Object();
private final String getLevelDir(final String dir_mipmaps, final int level) {
// synch, so that multithreaded generateMipMaps won't collide trying to create dirs
synchronized (FSLOCK) {
final String path = new StringBuilder(dir_mipmaps).append(level).append('/').toString();
if (isURL(dir_mipmaps)) {
return path;
}
final File file = new File(path);
if (file.exists() && file.isDirectory()) {
return path;
}
// else, create it
try {
file.mkdir();
return path;
} catch (Exception e) {
IJError.print(e);
return null;
}
}
}
/** Returns the near-unique folder for the project hosted by this FSLoader. */
public String getUNUIdFolder() {
return new StringBuilder(getStorageFolder()).append("trakem2.").append(unuid).append('/').toString();
}
/** Return the unuid_dir or null if none valid selected. */
private String obtainUNUIdFolder() {
YesNoCancelDialog yn = ControlWindow.makeYesNoCancelDialog("Old .xml version!", "The loaded XML file does not contain an UNUId. Select a shared UNUId folder?\nShould look similar to: trakem2.12345678.12345678.12345678");
if (!yn.yesPressed()) return null;
DirectoryChooser dc = new DirectoryChooser("Select UNUId folder");
String unuid_dir = dc.getDirectory();
String unuid_dir_name = new File(unuid_dir).getName();
Utils.log2("Selected UNUId folder: " + unuid_dir + "\n with name: " + unuid_dir_name);
if (null != unuid_dir) {
if (IJ.isWindows()) unuid_dir = unuid_dir.replace('\\', '/');
if ( ! unuid_dir_name.startsWith("trakem2.")) {
Utils.logAll("Invalid UNUId folder: must start with \"trakem2.\". Try again or cancel.");
return obtainUNUIdFolder();
} else {
String[] nums = unuid_dir_name.split("\\.");
if (nums.length != 4) {
Utils.logAll("Invalid UNUId folder: needs trakem + 3 number blocks. Try again or cancel.");
return obtainUNUIdFolder();
}
for (int i=1; i<nums.length; i++) {
try {
Long.parseLong(nums[i]);
} catch (NumberFormatException nfe) {
Utils.logAll("Invalid UNUId folder: at least one block is not a number. Try again or cancel.");
return obtainUNUIdFolder();
}
}
// ok, aceptamos pulpo
String unuid = unuid_dir_name.substring(8); // remove prefix "trakem2."
if (unuid.endsWith("/")) unuid = unuid.substring(0, unuid.length() -1);
this.unuid = unuid;
if (!unuid_dir.endsWith("/")) unuid_dir += "/";
String dir_storage = new File(unuid_dir).getParent().replace('\\', '/');
if (!dir_storage.endsWith("/")) dir_storage += "/";
this.dir_storage = dir_storage;
this.dir_mipmaps = unuid_dir + "trakem2.mipmaps/";
return unuid_dir;
}
}
return null;
}
/** If parent path is null, it's asked for.*/
private boolean createMipMapsDir(String parent_path) {
if (null == this.unuid) this.unuid = createUNUId(parent_path);
if (null == parent_path) {
// try to create it in the same directory where the XML file is
if (null != dir_storage) {
File f = new File(getUNUIdFolder() + "/trakem2.mipmaps");
if (!f.exists()) {
try {
if (f.mkdir()) {
this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
return true;
}
} catch (Exception e) {}
} else if (f.isDirectory()) {
this.dir_mipmaps = f.getAbsolutePath().replace('\\', '/');
if (!dir_mipmaps.endsWith("/")) this.dir_mipmaps += "/";
return true;
}
// else can't use it
}
// else, ask for a new folder
final DirectoryChooser dc = new DirectoryChooser("Select MipMaps parent directory");
parent_path = dc.getDirectory();
if (null == parent_path) return false;
if (IJ.isWindows()) parent_path = parent_path.replace('\\', '/');
if (!parent_path.endsWith("/")) parent_path += "/";
}
// examine parent path
final File file = new File(parent_path);
if (file.exists()) {
if (file.isDirectory()) {
// all OK
this.dir_mipmaps = parent_path + "trakem2." + unuid + "/trakem2.mipmaps/";
try {
File f = new File(this.dir_mipmaps);
f.mkdirs();
if (!f.exists()) {
Utils.log("Could not create trakem2.mipmaps!");
return false;
}
} catch (Exception e) {
IJError.print(e);
return false;
}
} else {
Utils.showMessage("Selected parent path is not a directory. Please choose another one.");
return createMipMapsDir(null);
}
} else {
Utils.showMessage("Parent path does not exist. Please select a new one.");
return createMipMapsDir(null);
}
return true;
}
/** Remove all mipmap images from the cache, and optionally set the dir_mipmaps to null. */
public void flushMipMaps(boolean forget_dir_mipmaps) {
if (null == dir_mipmaps) return;
synchronized (db_lock) {
try {
if (forget_dir_mipmaps) this.dir_mipmaps = null;
mawts.removeAndFlushAll();
} catch (Throwable t) {
handleCacheError(t);
}
}
}
/** Remove from the cache all images of level larger than zero corresponding to the given patch id. */
public void flushMipMaps(final long id) {
if (null == dir_mipmaps) return;
synchronized (db_lock) {
try {
mawts.removeAndFlushPyramid(id);
} catch (Throwable t) {
handleCacheError(t);
}
}
}
/** Gets data from the Patch and queues a new task to do the file removal in a separate task manager thread. */
public Future<Boolean> removeMipMaps(final Patch p) {
return removeMipMaps(p, mExt);
}
private Future<Boolean> removeMipMaps(final Patch p, final String extension) {
if (null == dir_mipmaps) return null;
// cache values before they are changed:
final int width = (int)p.getWidth();
final int height = (int)p.getHeight();
return remover.submit(new Callable<Boolean>() {
public Boolean call() {
try {
final String path = getAbsolutePath(p);
if (null == path) {
// missing file
Utils.log2("Remover: null path for Patch " + p);
return false;
}
removeMipMaps(createIdPath(Long.toString(p.getId()), new File(path).getName(), extension), width, height);
flushMipMaps(p.getId());
return true;
} catch (Exception e) {
IJError.print(e);
}
return false;
}
});
}
private void removeMipMaps(final String filename, final int width, final int height) {
int w = width;
int h = height;
int k = 0; // the level
do {
final File f = new File(new StringBuilder(dir_mipmaps).append(k).append('/').append(filename).toString());
if (f.exists()) {
try {
if (!f.delete()) {
Utils.log2("Could not remove file " + f.getAbsolutePath());
}
} catch (Exception e) {
IJError.print(e);
}
}
w /= 2;
h /= 2;
k++;
} while (w >= 32 && h >= 32); // not smaller than 32x32
}
@Override
public boolean usesMipMapsFolder() {
return null != dir_mipmaps;
}
/** Return the closest level to {@code level} that exists as a file.
* If no valid path is found for the patch, returns ERROR_PATH_NOT_FOUND.
*/
@Override
public int getClosestMipMapLevel(final Patch patch, int level, final int max_level) {
if (null == dir_mipmaps) return 0;
try {
final String path = getAbsolutePath(patch);
if (null == path) return ERROR_PATH_NOT_FOUND;
final String filename = new File(path).getName() + mExt;
if (isURL(dir_mipmaps)) {
if (level <= 0) return 0;
// choose the smallest dimension
// find max level that keeps dim over 32 pixels
if (level > max_level) return max_level;
return level;
} else {
do {
final File f = new File(new StringBuilder(dir_mipmaps).append(level).append('/').append(filename).toString());
if (f.exists()) {
return level;
}
// try the next level
level--;
} while (level >= 0);
}
} catch (Exception e) {
IJError.print(e);
}
return 0;
}
/** A temporary list of Patch instances for which a pyramid is being generated.
* Access is synchronized by gm_lock. */
final private Map<Patch,Future<Boolean>> regenerating_mipmaps = new HashMap<Patch,Future<Boolean>>();
/** A lock for the generation of mipmaps. */
final private Object gm_lock = new Object();
/** Checks if the mipmap file for the Patch and closest upper level to the desired magnification exists. */
public boolean checkMipMapFileExists(final Patch p, final double magnification) {
if (null == dir_mipmaps) return false;
final int level = getMipMapLevel(magnification, maxDim(p));
if (isURL(dir_mipmaps)) return true; // just assume that it does
if (new File(dir_mipmaps + level + "/" + new File(getAbsolutePath(p)).getName() + "." + p.getId() + mExt).exists()) return true;
return false;
}
final Set<Patch> cannot_regenerate = Collections.synchronizedSet(new HashSet<Patch>());
/** 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 release memory, avoiding locking on the db_lock. */
protected MipMapImage fetchMipMapAWT(final Patch patch, final int level, final long n_bytes) {
return fetchMipMapAWT(patch, level, n_bytes, 0);
}
/** Does the actual fetching of the file. Returns null if the file does not exist.
* Does NOT pre-release memory from the cache;
* call releaseToFit to do that. */
public final MipMapImage fetchMipMap(final Patch patch, int level, final long n_bytes) {
final int max_level = getHighestMipMapLevel(patch);
if ( level > max_level ) level = max_level;
final double scale = Math.pow( 2.0, level );
final String filename = getInternalFileName(patch);
if (null == filename) {
Utils.log2("null internal filename!");
return null;
}
// New style:
final String path = new StringBuilder(dir_mipmaps).append( level ).append('/').append(createIdPath(Long.toString(patch.getId()), filename, mExt)).toString();
//releaseToFit(n_bytes * 8); // eight times, for the jpeg decoder alloc/dealloc at least 2 copies, and with alpha even one more
// TODO the x8 is overly exaggerated
if ( patch.hasAlphaChannel() ) {
final Image img = mmio.open( path );
return img == null ? null : new MipMapImage( img, scale, scale );
} else if ( patch.paintsWithFalseColor() ) {
// AKA Patch has a LUT or is LUT image like a GIF
final Image img = mmio.open( path );
return img == null ? null : new MipMapImage( img, scale, scale ); // considers c_alphas
} else {
final Image img;
switch (patch.getType()) {
case ImagePlus.GRAY16:
case ImagePlus.GRAY8:
case ImagePlus.GRAY32:
img = mmio.openGrey( path ); // ImageSaver.openGreyJpeg(path);
return img == null ? null : new MipMapImage( img, scale, scale );
default:
// For color images: (considers URL as well)
img = mmio.open( path );
return img == null ? null : new MipMapImage( img, scale, scale ); // considers c_alphas
}
}
}
/** Will NOT free memory. */
private final MipMapImage fetchMipMapAWT(final Patch patch, final int level, final long n_bytes, final int retries) {
if (null == dir_mipmaps) {
Utils.log2("null dir_mipmaps");
return null;
}
while (retries < MAX_RETRIES) {
try {
// TODO should wait if the file is currently being generated
final MipMapImage mipMap = fetchMipMap(patch, level, n_bytes);
if (null != mipMap) return mipMap;
// if we got so far ... try to regenerate the mipmaps
if (!mipmaps_regen) {
return null;
}
// check that REALLY the file doesn't exist.
if (cannot_regenerate.contains(patch)) {
Utils.log("Cannot regenerate mipmaps for patch " + patch);
return null;
}
//Utils.log2("getMipMapAwt: imp is " + imp + " for path " + dir_mipmaps + level + "/" + new File(getAbsolutePath(patch)).getName() + "." + patch.getId() + mExt);
// Regenerate in the case of not asking for an image under 32x32
double scale = 1 / Math.pow(2, level);
if (level >= 0 && patch.getWidth() * scale >= 32 && patch.getHeight() * scale >= 32 && isMipMapsRegenerationEnabled()) {
// regenerate in a separate thread
regenerateMipMaps( patch );
return new MipMapImage( REGENERATING, patch.getWidth() / REGENERATING.getWidth(), patch.getHeight() / REGENERATING.getHeight() );
}
} catch (OutOfMemoryError oome) {
Utils.log2("fetchMipMapAWT: recovering from OutOfMemoryError");
recoverOOME();
Thread.yield();
// Retry:
return fetchMipMapAWT(patch, level, n_bytes, retries + 1);
} catch (Throwable t) {
IJError.print(t);
}
}
return null;
}
static private AtomicInteger n_regenerating = new AtomicInteger(0);
static private ExecutorService regenerator = null;
static private ExecutorService remover = null;
static public ExecutorService repainter = null;
static private int nStaticServiceThreads = nStaticServiceThreads();
static public ScheduledExecutorService autosaver = null;
static private final class DONE implements Future<Boolean>
{
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return true;
}
@Override
public Boolean get() throws InterruptedException, ExecutionException {
return true;
}
@Override
public Boolean get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException,
TimeoutException {
return true;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return true;
}
};
/** Queue the regeneration of mipmaps for the Patch; returns immediately, having submitted the job to an executor queue;
* returns a Future if the task was submitted, null if not. */
@Override
public final Future<Boolean> regenerateMipMaps(final Patch patch) {
if (!isMipMapsRegenerationEnabled()) {
// If not enabled, the cache must be flushed
flushMipMaps(patch.getId());
return new DONE();
}
synchronized (gm_lock) {
try {
Future<Boolean> fu = regenerating_mipmaps.get(patch);
if (null != fu) return fu;
// else, start it
n_regenerating.incrementAndGet();
Utils.log2("SUBMITTING to regen " + patch);
Utils.showStatus(new StringBuilder("Regenerating mipmaps (").append(n_regenerating.get()).append(" to go)").toString());
// Eliminate existing mipmaps, if any, in a separate thread:
//Utils.log2("calling removeMipMaps from regenerateMipMaps");
final Future<Boolean> removing = removeMipMaps(patch);
fu = regenerator.submit(new Callable<Boolean>() {
public Boolean call() {
boolean b = false;
try {
// synchronize with the removal:
if (null != removing) removing.get();
Utils.showStatus(new StringBuilder("Regenerating mipmaps (").append(n_regenerating.get()).append(" to go)").toString());
b = generateMipMaps(patch); // will remove the Future from the regenerating_mipmaps table, under proper gm_lock synchronization
Display.repaint(patch.getLayer());
Display.updatePanel(patch.getLayer(), patch);
Utils.showStatus("");
} catch (Exception e) {
IJError.print(e);
}
n_regenerating.decrementAndGet();
return b;
}
});
regenerating_mipmaps.put(patch, fu);
return fu;
} catch (Exception e) {
IJError.print(e);
return null;
}
}
}
/** 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.
public long estimateImageFileSize(final Patch p, final int level) {
if (level > 0) {
// jpeg image to be loaded:
final double scale = 1 / Math.pow(2, level);
return (long)(p.getWidth() * scale * p.getHeight() * scale * 5 + 1024);
}
long size = (long)(p.getWidth() * p.getHeight());
int bytes_per_pixel = 1;
final int type = p.getType();
switch (type) {
case ImagePlus.GRAY32:
bytes_per_pixel = 5; // 4 for the FloatProcessor, and 1 for the pixels8 to make an image
break;
case ImagePlus.GRAY16:
bytes_per_pixel = 3; // 2 for the ShortProcessor, and 1 for the pixels8
case ImagePlus.COLOR_RGB:
bytes_per_pixel = 4;
break;
case ImagePlus.GRAY8:
case ImagePlus.COLOR_256:
bytes_per_pixel = 1;
// check jpeg, which can only encode RGB (taken care of above) and 8-bit and 8-bit color images:
String path = ht_paths.get(p.getId());
if (null != path && path.endsWith(mExt)) bytes_per_pixel = 5; //4 for the int[] and 1 for the byte[]
break;
default:
bytes_per_pixel = 5; // conservative
break;
}
return size * bytes_per_pixel + 1024;
}
public String makeProjectName() {
if (null == project_file_path || 0 == project_file_path.length()) return super.makeProjectName();
final String name = new File(project_file_path).getName();
final int i_dot = name.lastIndexOf('.');
if (-1 == i_dot) return name;
if (0 == i_dot) return super.makeProjectName();
return name.substring(0, i_dot);
}
/** Returns the path where the imp is saved to: the storage folder plus a name. */
public String handlePathlessImage(final ImagePlus imp) {
FileInfo fi = imp.getOriginalFileInfo();
if (null == fi) fi = imp.getFileInfo();
if (null == fi.fileName || fi.fileName.equals("")) {
fi.fileName = "img_" + System.currentTimeMillis() + ".tif";
}
if (!fi.fileName.endsWith(".tif")) fi.fileName += ".tif";
fi.directory = dir_storage;
if (imp.getNSlices() > 1) {
new FileSaver(imp).saveAsTiffStack(dir_storage + fi.fileName);
} else {
new FileSaver(imp).saveAsTiff(dir_storage + fi.fileName);
}
Utils.log2("Saved a copy into the storage folder:\n" + dir_storage + fi.fileName);
return dir_storage + fi.fileName;
}
/** Convert old-style storage folders to new style. */
public boolean fixStorageFolders() {
try {
// 1 - Create folder unuid_folder at storage_folder + unuid
if (null == this.unuid) {
Utils.log2("No unuid for project!");
return false;
}
// the trakem2.<unuid> folder that will now contain trakem2.mipmaps, trakem2.masks, etc.
final String unuid_folder = getUNUIdFolder();
File fdir = new File(unuid_folder);
if (!fdir.exists()) {
if (!fdir.mkdir()) {
Utils.log2("Could not create folder " + unuid_folder);
return false;
}
}
// 2 - Create trakem2.mipmaps inside unuid folder
final String new_dir_mipmaps = unuid_folder + "trakem2.mipmaps/";
fdir = new File(new_dir_mipmaps);
if (!fdir.mkdir()) {
Utils.log2("Could not create folder " + new_dir_mipmaps);
return false;
}
// 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.
final String dir_mipmaps = getMipMapsFolder();
for (final String name : new File(dir_mipmaps).list()) {
final String level_dir = new StringBuilder(dir_mipmaps).append(name).append('/').toString();
final File f = new File(level_dir);
if (!f.isDirectory() || f.isHidden()) continue;
for (final String mm : f.list()) {
if (!mm.endsWith(mExt)) continue;
// parse the mipmap file: filename + '.' + id + '.jpg'
int last_dot = mm.lastIndexOf('.');
if (-1 == last_dot) continue;
int prev_last_dot = mm.lastIndexOf('.', last_dot -1);
String id = mm.substring(prev_last_dot+1, last_dot);
String filename = mm.substring(0, prev_last_dot);
File oldf = new File(level_dir + mm);
File newf = new File(new StringBuilder(new_dir_mipmaps).append(name).append('/').append(createIdPath(id, filename, mExt)).toString());
File fd = newf.getParentFile();
fd.mkdirs();
if (!fd.exists()) {
Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
continue;
}
if (!oldf.renameTo(newf)) {
Utils.log2("Could not move mipmap file " + oldf.getAbsolutePath() + " to " + newf.getAbsolutePath());
continue;
}
}
}
// Set it!
this.dir_mipmaps = new_dir_mipmaps;
// Remove old empty dirs:
Utils.removeFile(new File(dir_mipmaps));
// 4 - same for alpha folder and features folder.
final String masks_folder = getStorageFolder() + "trakem2.masks/";
File fmasks = new File(masks_folder);
this.dir_masks = null;
if (fmasks.exists()) {
final String new_dir_masks = unuid_folder + "trakem2.masks/";
final File[] fmask_files = fmasks.listFiles();
if (null != fmask_files) { // can be null if there are no files inside fmask directory
for (final File fmask : fmask_files) {
final String name = fmask.getName();
if (!name.endsWith(".zip")) continue;
int last_dot = name.lastIndexOf('.');
if (-1 == last_dot) continue;
int prev_last_dot = name.lastIndexOf('.', last_dot -1);
String id = name.substring(prev_last_dot+1, last_dot);
String filename = name.substring(0, prev_last_dot);
File newf = new File(new_dir_masks + createIdPath(id, filename, ".zip"));
File fd = newf.getParentFile();
fd.mkdirs();
if (!fd.exists()) {
Utils.log2("Could not create parent dir " + fd.getAbsolutePath());
continue;
}
if (!fmask.renameTo(newf)) {
Utils.log2("Could not move mask file " + fmask.getAbsolutePath() + " to " + newf.getAbsolutePath());
continue;
}
}
}
// Set it!
this.dir_masks = new_dir_masks;
// remove old empty:
Utils.removeFile(fmasks);
}
// TODO should save the .xml file, so the unuid and the new storage folders are set in there!
return true;
} catch (Exception e) {
IJError.print(e);
}
return false;
}
/** For Patch id=12345 creates 12/34/5.${filename}.jpg */
static public final String createMipMapRelPath(final Patch p, final String ext) {
return createIdPath(Long.toString(p.getId()), new File(p.getCurrentPath()).getName(), ext);
}
/** For sid=12345 creates 12/34/5.${filename}.jpg
* Will be fine with other filename-valid chars in sid. */
static public final String createIdPath(final String sid, final String filename, final String ext) {
final StringBuilder sf = new StringBuilder(((sid.length() * 3) / 2) + 1);
final int len = sid.length();
for (int i=1; i<=len; i++) {
sf.append(sid.charAt(i-1));
if (0 == i % 2 && len != i) sf.append('/');
}
return sf.append('.').append(filename).append(ext).toString();
}
public String getUNUId() {
return unuid;
}
/** Waits until a proper image of the desired size or larger can be returned, which is never the Loader.REGENERATING image.
* If no image can be loaded, returns Loader.NOT_FOUND.
* If the Patch is undergoing mipmap regeneration, it waits until done.
*/
@Override
public MipMapImage fetchDataImage( final Patch p, final double mag) {
Future<Boolean> fu = null;
MipMapImage mipMap = null;
synchronized (gm_lock) {
fu = regenerating_mipmaps.get(p);
}
if (null == fu) {
// Patch is currently not under regeneration
mipMap = fetchImage( p, mag );
// If the patch mipmaps didn't exist,
// the call to fetchImage will trigger mipmap regeneration
// and img will be now Loader.REGENERATING
if (Loader.REGENERATING != mipMap.image ) {
return mipMap;
} else {
synchronized (gm_lock) {
fu = regenerating_mipmaps.get(p);
}
}
}
if (null != fu) {
try {
if ( ! fu.get()) {
Utils.log("Loader.fetchDataImage: could not regenerate mipmaps and get an image for patch " + p);
return new MipMapImage( NOT_FOUND, p.getWidth() / NOT_FOUND.getWidth(), p.getHeight() / NOT_FOUND.getHeight() );
}
// Now the image should be good:
mipMap = fetchImage(p, mag);
// Check in any case:
if (Loader.isSignalImage(mipMap.image)) {
// Attempt to create from scratch
return new MipMapImage( p.createTransformedImage().createImage(p.getMin(), p.getMax()), 1, 1);
} else {
return mipMap;
}
} catch (Throwable e) {
IJError.print(e);
}
}
// else:
Utils.log( "Loader.fetchDataImage: could not get a data image for patch " + p );
return new MipMapImage( NOT_FOUND, p.getWidth() / NOT_FOUND.getWidth(), p.getHeight() / NOT_FOUND.getHeight() );
}
public ImagePlus fetchImagePlus( Stack stack )
{
ImagePlus imp = null;
String path = null;
ImageLoadingLock plock = null;
synchronized (db_lock) {
try {
imp = mawts.get(stack.getId());
if (null != imp) {
return imp;
}
path = stack.getFilePath();
/* not cached */
plock = getOrMakeImageLoadingLock( stack.getId(), 0 );
} catch (Throwable t) {
handleCacheError(t);
return null;
}
}
synchronized (plock) {
imp = mawts.get( stack.getId());
if (null != imp) {
// was loaded by a different thread
synchronized (db_lock) {
removeImageLoadingLock(plock);
}
return imp;
}
// going to load:
releaseToFit(stack.estimateImageFileSize());
imp = openImage(getAbsolutePath(path));
//preProcess(p, imp);
synchronized (db_lock) {
try {
if (null == imp) {
if (!hs_unloadable.contains(stack)) {
Utils.log("FSLoader.fetchImagePlus: no image exists for stack " + stack + " at path " + path);
hs_unloadable.add( stack );
}
// if (ControlWindow.isGUIEnabled()) {
// /* TODO offer repair for more things than patches */
// FilePathRepair.add( stack );
// }
return null;
} else {
mawts.put( stack.getId(), imp, (int)Math.max(stack.getWidth(), stack.getHeight()));
}
} catch (Exception e) {
IJError.print(e);
} finally {
removeImageLoadingLock(plock);
}
return imp;
}
}
}
/**
* Delete stale files under the {@link FSLoader#unuid} folder.
* These include "*.ct" files (for {@link CoordinateTransform})
* and "*.zip" files (for alpha mask images) that are not referenced from any {@link Patch}.
*/
@Override
public boolean deleteStaleFiles(boolean coordinate_transforms, boolean alpha_masks) {
boolean b = true;
final Project project = Project.findProject(this);
if (coordinate_transforms) b = b && StaleFiles.deleteCoordinateTransforms(project);
if (alpha_masks) b = b && StaleFiles.deleteAlphaMasks(project);
return b;
}
////////////////////
static final public String[] MIPMAP_FORMATS = new String[]{".jpg", ".png", ".tif", ".raw", ".rag"};
static public final int MIPMAP_JPEG = 0;
static public final int MIPMAP_PNG = 1;
static public final int MIPMAP_TIFF = 2;
static public final int MIPMAP_RAW = 3;
static public final int MIPMAP_RAG = 4;
static private final int MIPMAP_HIGHEST = MIPMAP_RAG; // WARNING: update this value if other formats are added
// Default: RAG
private int mipmaps_format = MIPMAP_RAG;
private String mExt = MIPMAP_FORMATS[mipmaps_format]; // the extension currently in use
private RWImage mmio = new RWImageRag();
private RWImage newMipMapRWImage() {
switch (this.mipmaps_format) {
case MIPMAP_JPEG:
return new RWImageJPG();
case MIPMAP_PNG:
return new RWImagePNG();
case MIPMAP_TIFF:
return new RWImageTIFF();
case MIPMAP_RAW:
return new RWImageRaw();
case MIPMAP_RAG:
return new RWImageRag();
// WARNING add here another one
}
return null;
}
/** Any of: {@link #MIPMAP_JPEG}, {@link #MIPMAP_PNG}, {@link #MIPMAP_TIFF}, {@link #MIPMAP_RAW},
* {@link #MIPMAP_RAG}. */
@Override
public final int getMipMapFormat() {
return mipmaps_format;
}
@Override
public final boolean setMipMapFormat(final int format) {
switch (format) {
case MIPMAP_JPEG:
case MIPMAP_PNG:
case MIPMAP_TIFF:
case MIPMAP_RAW:
case MIPMAP_RAG:
this.mipmaps_format = format;
this.mExt = MIPMAP_FORMATS[mipmaps_format];
this.mmio = newMipMapRWImage();
return true;
default:
Utils.log("Ignoring unknown mipmap format: " + format);
return false;
}
}
/** Removes all mipmap files and recreates them with the currently set mipmaps format.
* @param old_format Any of MIPMAP_JPEG, MIPMAP_PNG in which files were saved before. */
@Override
public Bureaucrat updateMipMapsFormat(final int old_format, final int new_format) {
if (old_format < 0 || old_format > MIPMAP_HIGHEST) {
Utils.log("Invalid old format for mipmaps!");
return null;
}
if (!setMipMapFormat(new_format)) {
Utils.log("Invalid new format for mipmaps!");
return null;
}
final Project project = Project.findProject(FSLoader.this);
return Bureaucrat.createAndStart(new Worker.Task("Updating mipmaps format") {
public void exec() {
try {
final List<Future<?>> fus = new ArrayList<Future<?>>();
final String ext = MIPMAP_FORMATS[old_format];
for (Layer la : project.getRootLayerSet().getLayers()) {
for (Displayable p : la.getDisplayables(Patch.class)) {
fus.add(removeMipMaps((Patch)p, ext));
}
}
Utils.wait(fus);
fus.clear();
for (Layer la : project.getRootLayerSet().getLayers()) {
for (Displayable p : la.getDisplayables(Patch.class)) {
fus.add(regenerateMipMaps((Patch)p));
}
}
Utils.wait(fus);
} catch (Exception e) {
IJError.print(e);
}
}
}, project);
}
private abstract class RWImage {
boolean save(ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
if (as_grey) ip = ip.convertToByte(false);
if (ip instanceof ByteProcessor) {
return save(path, new byte[][]{(byte[])ip.getPixels()}, ip.getWidth(), ip.getHeight(), quality);
} else if (ip instanceof ColorProcessor) {
final int[] p = (int[]) ip.getPixels();
final byte[] r = new byte[p.length],
g = new byte[p.length],
b = new byte[p.length],
a = new byte[p.length];
for (int i=0; i<p.length; ++i) {
final int x = p[i];
r[i] = (byte)((x >> 16)&0xff);
g[i] = (byte)((x >> 8)&0xff);
b[i] = (byte) (x &0xff);
a[i] = (byte)((x >> 24)&0xff);
}
return save(path, new byte[][]{r, g, b, a}, ip.getWidth(), ip.getHeight(), quality);
}
return false;
}
boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
switch (bi.getType()) {
case BufferedImage.TYPE_BYTE_GRAY:
return save(new ByteProcessor(bi), path, quality, false);
default:
if (as_grey) return save(new ByteProcessor(bi), path, quality, false);
return save(new ColorProcessor(bi), path, quality, false);
}
}
abstract boolean save(String path, byte[][] b, int width, int height, float quality);
/** Opens grey, RGB and RGBA. */
abstract BufferedImage open(String path);
/** Opens grey images or, if not grey, converts them to grey. */
abstract BufferedImage openGrey(String path);
}
private final class RWImageJPG extends RWImage {
@Override
final boolean save(final ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
return ImageSaver.saveAsJpeg(ip, path, quality, as_grey);
}
@Override
final boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
return ImageSaver.saveAsJpeg(bi, path, quality, as_grey);
}
@Override
final BufferedImage open(String path) {
return ImageSaver.openImage(path, true);
}
@Override
final BufferedImage openGrey(final String path) {
return ImageSaver.open(path, true);
}
@Override
final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
switch (b.length) {
case 1:
return ImageSaver.saveAsGreyJpeg(b[0], width, height, path, quality);
case 2:
return ImageSaver.saveAsJpegAlpha(ImageSaver.createARGBImage(P.blend(b[0], b[1]), width, height), path, quality);
case 3:
return ImageSaver.saveAsJpeg(ImageSaver.createRGBImage(P.blend(b[0], b[1], b[2]), width, height), path, quality, false);
case 4:
return ImageSaver.saveAsJpegAlpha(ImageSaver.createARGBImage(P.blend(b[0], b[1], b[2], b[3]), width, height), path, quality);
}
return false;
}
}
private final class RWImagePNG extends RWImage {
@Override
final boolean save(final ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
return ImageSaver.saveAsPNG(ip, path);
}
@Override
final boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
return ImageSaver.saveAsPNG(bi, path);
}
@Override
final BufferedImage open(String path) {
return ImageSaver.openImage(path, true);
}
@Override
final BufferedImage openGrey(final String path) {
return ImageSaver.openGreyImage(path);
}
@Override
final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
BufferedImage bi = null;
try {
switch (b.length) {
case 1:
bi = ImageSaver.createGrayImage(b[0], width, height);
return ImageSaver.saveAsPNG(bi, path);
case 2:
bi = ImageSaver.createARGBImage(P.blend(b[0], b[1]), width, height);
return ImageSaver.saveAsPNG(bi, path);
case 3:
bi = ImageSaver.createRGBImage(P.blend(b[0], b[1], b[2]), width, height);
return ImageSaver.saveAsPNG(bi, path);
case 4:
bi = ImageSaver.createARGBImage(P.blend(b[0], b[1], b[2], b[3]), width, height);
return ImageSaver.saveAsPNG(bi, path);
}
} finally {
if (null != bi) {
bi.flush();
CachingThread.storeArrayForReuse(bi);
}
}
return false;
}
}
private final class RWImageTIFF extends RWImage {
@Override
final boolean save(final ImageProcessor ip, final String path, final float quality, final boolean as_grey) {
return ImageSaver.saveAsTIFF(ip, path, as_grey);
}
@Override
final boolean save(final BufferedImage bi, final String path, final float quality, final boolean as_grey) {
return ImageSaver.saveAsTIFF(bi, path, as_grey);
}
@Override
final BufferedImage openGrey(final String path) {
return ImageSaver.openGreyTIFF(path);
}
@Override
final BufferedImage open(String path) {
return ImageSaver.openTIFF(path, true);
}
@Override
final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
switch (b.length) {
case 1:
return ImageSaver.saveAsTIFF(ImageSaver.createGrayImage(b[0], width, height), path, false); // already grey
case 2:
return ImageSaver.saveAsTIFF(ImageSaver.createARGBImage(P.blend(b[0], b[1]), width, height), path, false);
case 3:
return ImageSaver.saveAsTIFF(ImageSaver.createRGBImage(P.blend(b[0], b[1], b[2]), width, height), path, false);
case 4:
return ImageSaver.saveAsTIFF(ImageSaver.createARGBImage(P.blend(b[0], b[1], b[2], b[3]), width, height), path, false);
}
return false;
}
}
private final class RWImageRaw extends RWImage {
@Override
final BufferedImage open(final String path) {
return RawMipMaps.read(path);
}
@Override
final BufferedImage openGrey(final String path) {
return ImageSaver.asGrey(RawMipMaps.read(path)); // TODO may not need the asGrey if all is correct
}
@Override
final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
try {
return RawMipMaps.save(path, b, width, height);
} finally {
CachingThread.storeForReuse(b);
}
}
}
private final class RWImageRag extends RWImage {
@Override
final BufferedImage open(final String path) {
return RagMipMaps.read(path);
}
@Override
final BufferedImage openGrey(final String path) {
return ImageSaver.asGrey(RagMipMaps.read(path)); // TODO may not need the asGrey if all is correct
}
@Override
final boolean save(final String path, final byte[][] b, final int width, final int height, final float quality) {
try {
return RagMipMaps.save(path, b, width, height);
} finally {
CachingThread.storeForReuse(b);
}
}
}
@SuppressWarnings("unchecked")
@Override
protected boolean mapIntensities(final Patch p, final ImagePlus imp) {
final ImagePlus coefficients = new Opener().openImage(
getUNUIdFolder() +
"trakem2.its/" +
createIdPath(Long.toString(p.getId()), "it", ".tif"));
if (coefficients == null)
return false;
final ImageProcessor ip = imp.getProcessor();
@SuppressWarnings({"rawtypes"})
final LinearIntensityMap<FloatType> map =
new LinearIntensityMap<FloatType>(
(FloatImagePlus)ImagePlusImgs.from(coefficients));
@SuppressWarnings("rawtypes")
Img img;
final long[] dims = new long[]{imp.getWidth(), imp.getHeight()};
switch (p.getType()) {
case ImagePlus.GRAY8:
case ImagePlus.COLOR_256: // this only works for continuous color tables
img = ArrayImgs.unsignedBytes((byte[])ip.getPixels(), dims);
break;
case ImagePlus.GRAY16:
img = ArrayImgs.unsignedShorts((short[])ip.getPixels(), dims);
break;
case ImagePlus.COLOR_RGB:
img = ArrayImgs.argbs((int[])ip.getPixels(), dims);
break;
case ImagePlus.GRAY32:
img = ArrayImgs.floats((float[])ip.getPixels(), dims);
break;
default:
img = null;
}
if (img == null)
return false;
map.run(img);
return true;
}
@Override
public boolean clearIntensityMap(final Patch p) {
final File coefficients = new File(
getUNUIdFolder() +
"trakem2.its/" +
createIdPath(Long.toString(p.getId()), "it", ".tif"));
return coefficients.delete();
}
}