/** 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 as 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; import ij.IJ; import ij.gui.GenericDialog; import ij.io.DirectoryChooser; import ini.trakem2.display.AreaList; import ini.trakem2.display.AreaTree; import ini.trakem2.display.Ball; import ini.trakem2.display.Bucket; import ini.trakem2.display.Connector; import ini.trakem2.display.DLabel; import ini.trakem2.display.Display; import ini.trakem2.display.Displayable; import ini.trakem2.display.Dissector; import ini.trakem2.display.Layer; import ini.trakem2.display.LayerSet; import ini.trakem2.display.Patch; import ini.trakem2.display.Pipe; import ini.trakem2.display.Polyline; import ini.trakem2.display.Profile; import ini.trakem2.display.Stack; import ini.trakem2.display.Treeline; import ini.trakem2.display.YesNoDialog; import ini.trakem2.display.ZDisplayable; import ini.trakem2.persistence.DBLoader; import ini.trakem2.persistence.DBObject; import ini.trakem2.persistence.FSLoader; import ini.trakem2.persistence.Loader; import ini.trakem2.persistence.XMLOptions; import ini.trakem2.plugin.TPlugIn; import ini.trakem2.tree.DNDTree; import ini.trakem2.tree.LayerThing; import ini.trakem2.tree.LayerTree; import ini.trakem2.tree.ProjectThing; import ini.trakem2.tree.ProjectTree; import ini.trakem2.tree.TemplateThing; import ini.trakem2.tree.TemplateTree; import ini.trakem2.tree.Thing; import ini.trakem2.utils.Bureaucrat; import ini.trakem2.utils.IJError; import ini.trakem2.utils.ProjectToolbar; import ini.trakem2.utils.Search; import ini.trakem2.utils.Utils; import ini.trakem2.utils.Worker; import java.awt.Rectangle; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.Vector; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.swing.JTree; import javax.swing.UIManager; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; /** The top-level class in control. */ public class Project extends DBObject { static { try { //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); if (IJ.isLinux()) { UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); if (null != IJ.getInstance()) javax.swing.SwingUtilities.updateComponentTreeUI(IJ.getInstance()); //if ("albert".equals(System.getProperty("user.name"))) UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel"); } } catch (Exception e) { Utils.log("Failed to set System Look and Feel"); } } static final private Vector<PlugInSource> PLUGIN_SOURCES = new Vector<PlugInSource>(); static private class PlugInSource implements Comparable<PlugInSource> { String menu; Class<?> c; String title; PlugInSource(String menu, Class<?> c, String title) { this.menu = menu; this.c = c; this.title = title; } public int compareTo(PlugInSource ob) { return ob.title.compareTo(this.title); } } static { // Search for plugins under fiji/plugins directory jar files new Thread() { public void run() { try { setPriority(Thread.NORM_PRIORITY); setContextClassLoader(ij.IJ.getClassLoader()); final String plugins_dir = Utils.fixDir(ij.Menus.getPlugInsPath()); synchronized (PLUGIN_SOURCES) { for (String name : new File(plugins_dir).list()) { File f = new File(name); if (f.isHidden() || !name.toLowerCase().endsWith(".jar")) continue; JarFile jar = new JarFile(plugins_dir + name); JarEntry entry = null; for (Enumeration<JarEntry> en = jar.entries(); en.hasMoreElements(); ) { JarEntry je = en.nextElement(); if (je.getName().endsWith(".trakem2")) { entry = je; break; } } if (entry == null) continue; // Parse: BufferedReader br = new BufferedReader(new InputStreamReader(jar.getInputStream(entry))); try { while (true) { String line = br.readLine(); if (null == line) break; if (line.startsWith("#")) continue; // tokenize: // - from start to first comma is the menu // - from first comma to last comma is the title // - from last comma to end is the class // The above allows for commas to be inside the title int fc = line.indexOf(','); if (-1 == fc) continue; int lc = line.lastIndexOf(','); if (-1 == lc) continue; String menu = line.substring(0, fc).trim(); if (!menu.equals("Project Tree") && !menu.equals("Display")) continue; String classname = line.substring(lc+1).trim(); try { Class.forName(classname); } catch (ClassNotFoundException cnfe) { Utils.log2("TPlugIn class not found: " + classname); continue; } int fq = line.indexOf('"', fc); if (-1 == fq) continue; int lq = line.lastIndexOf('"', lc); if (-1 == lq) continue; String title = line.substring(fq+1, lq).trim(); try { PLUGIN_SOURCES.add(new PlugInSource(menu, Class.forName(classname), title)); Utils.log2("Found plugin for menu " + menu + " titled " + title + " for class " + classname); } catch (ClassNotFoundException cnfe) { Utils.log("Could not find TPlugIn class " + classname); } } } finally { br.close(); } }} } catch (Throwable t) { Utils.log("ERROR while parsing TrakEM2 plugins:"); IJError.print(t); }}}.start(); } /** Map of title keys vs TPlugin instances. */ private Map<PlugInSource,TPlugIn> plugins = null; /** Create plugin instances for this project. */ synchronized private Map<PlugInSource,TPlugIn> createPlugins() { final Map<PlugInSource,TPlugIn> m = Collections.synchronizedMap(new TreeMap<PlugInSource,TPlugIn>()); synchronized (PLUGIN_SOURCES) { for (PlugInSource source : PLUGIN_SOURCES) { try { m.put(source, (TPlugIn)source.c.newInstance()); } catch (Exception e) { Utils.log("ERROR initializing plugin!\nParsed tokens: [" + source.menu + "][" + source.title + "][" + source.c.getName() + "]"); IJError.print(e); } } } return m; } synchronized public TreeMap<String,TPlugIn> getPlugins(final String menu) { final TreeMap<String,TPlugIn> m = new TreeMap<String,TPlugIn>(); if (null == plugins) plugins = createPlugins(); // to be created the first time it's asked for for (Map.Entry<PlugInSource,TPlugIn> e : plugins.entrySet()) { if (e.getKey().menu.equals(menu)) m.put(e.getKey().title, e.getValue()); } return m; } /* // using virtual frame buffer instead, since the trees are needed public static final boolean headless = isHeadless(); private static boolean isHeadless() { return Boolean.parseBoolean(System.getProperty("java.awt.headless")); } */ /** Keep track of all open projects. */ static private ArrayList<Project> al_open_projects = new ArrayList<Project>(); private Loader loader; private TemplateTree template_tree = null; private ProjectTree project_tree = null; /** The root Thing that holds the project. */ private ProjectThing root_pt; /** The root LayerThing of the LayerTree. */ private LayerThing root_lt; /** The root TemplateThing of the TemplateTree. */ private TemplateThing root_tt; /** The root LayerSet that holds the layers. */ private LayerSet layer_set; static private TemplateThing layer_template = null; static private TemplateThing layer_set_template = null; /** The table of unique TemplateThing types; the key is the type (String). */ private final Map<String,TemplateThing> ht_unique_tt = Collections.synchronizedMap(new HashMap<String,TemplateThing>()); private LayerTree layer_tree = null; private String title = "Project"; private final HashMap<String,String> ht_props = new HashMap<String,String>(); private int mipmaps_mode = Loader.DEFAULT_MIPMAPS_MODE; /** The constructor used by the static methods present in this class. */ private Project(Loader loader) { super(loader); ControlWindow.getInstance(); // init this.loader = loader; this.project = this; // for the superclass DBObject loader.addToDatabase(this); } /** Constructor used by the Loader to find projects. These projects contain no loader. */ public Project(long id, String title) { super(null, id); ControlWindow.getInstance(); // init this.title = title; this.project = this; } private ScheduledFuture<?> autosaving = null; private void restartAutosaving() { // cancel current autosaving if it's running if (null != autosaving) try { autosaving.cancel(true); } catch (Throwable t) { IJError.print(t); } // final int interval_in_minutes = getProperty("autosaving_interval", 0); if (0 == interval_in_minutes) return; // else, relaunch this.autosaving = FSLoader.autosaver.scheduleWithFixedDelay(new Runnable() { public void run() { try { if (loader.hasChanges()) { Bureaucrat.createAndStart(new Worker.Task("auto-saving") { @Override public void exec() { Project.this.save(); } }, Project.this).join(); } } catch (Throwable e) { Utils.log("*** Autosaver failed:"); IJError.print(e); } } }, interval_in_minutes * 60, interval_in_minutes * 60, TimeUnit.SECONDS); } static public Project getProject(final String title) { for (final Project pr : al_open_projects) { if (pr.title.equals(title)) return pr; } return null; } /** Return a copy of the list of all open projects. */ static public ArrayList<Project> getProjects() { return new ArrayList<Project>(al_open_projects); } /** Create a new PostgreSQL-based TrakEM2 project. */ static public Project newDBProject() { if (Utils.wrongImageJVersion()) return null; // create DBLoader loader = new DBLoader(); // check connection settings if (!loader.isReady()) return null; // check connection if (!loader.isConnected()) { Utils.showMessage("Can't talk to database."); return null; } return createNewProject(loader, true); } /** Open a TrakEM2 project from the database. Queries the database for existing projects and if more than one, asks which one to open. */ static public Project openDBProject() { if (Utils.wrongImageJVersion()) return null; DBLoader loader = new DBLoader(); if (!loader.isReady()) return null; // check connection if (!loader.isConnected()) { Utils.showMessage("Can't talk to database."); loader.destroy(); return null; } // query the database for existing projects Project[] projects = loader.getProjects(); if (null == projects) { Utils.showMessage("Can't talk to database (null list of projects)."); loader.destroy(); return null; } Project project = null; if (0 == projects.length) { Utils.showMessage("No projects in this database."); loader.destroy(); return null; } else if (1 == projects.length) { project = projects[0]; } else { // ask to choose one String[] titles = new String[projects.length]; for (int i=0; i<projects.length; i++) { titles[i] = projects[i].title; } GenericDialog gd = new GenericDialog("Choose"); gd.addMessage("Choose project to open:"); gd.addChoice("project: ", titles, titles[titles.length -1]); gd.showDialog(); if (gd.wasCanceled()) { loader.destroy(); return null; } project = projects[gd.getNextChoiceIndex()]; } // check if the selected project is open already for (final Project p : al_open_projects) { if (loader.isIdenticalProjectSource(p.loader) && p.id == project.id && p.title.equals(project.title)) { Utils.showMessage("A project with title " + p.title + " and id " + p.id + " from the same database is already open."); loader.destroy(); return null; } } // now, open the selected project // assign loader project.loader = loader; // grab the XML template TemplateThing template_root = loader.getTemplateRoot(project); if (null == template_root) { Utils.showMessage("Failed to retrieve the template tree."); project.destroy(); return null; } project.template_tree = new TemplateTree(project, template_root); synchronized (project.ht_unique_tt) { project.ht_unique_tt.clear(); project.ht_unique_tt.putAll(template_root.getUniqueTypes(new HashMap<String,TemplateThing>())); } // create the project Thing, to be root of the whole user Thing tree (and load all its objects) HashMap<Long,Displayable> hs_d = new HashMap<Long,Displayable>(); // to collect all created displayables, and then reassign to the proper layers. try { // create a template for the project Thing TemplateThing project_template = new TemplateThing("project"); project.ht_unique_tt.put("project", project_template); project_template.addChild(template_root); project.root_pt = loader.getRootProjectThing(project, template_root, project_template, hs_d); // restore parent/child and attribute ownership and values (now that all Things exist) project.root_pt.setup(); } catch (Exception e) { Utils.showMessage("Failed to retrieve the Thing tree for the project."); IJError.print(e); project.destroy(); return null; } // create the user objects tree project.project_tree = new ProjectTree(project, project.root_pt); // restore the expanded state of each node loader.restoreNodesExpandedState(project); // create the layers templates project.createLayerTemplates(); // fetch the root layer thing and the root layer set (will load all layers and layer sets, with minimal contents of patches; gets the basic objects -profile, pipe, etc.- from the project.root_pt). Will open all existing displays for each layer. LayerThing root_layer_thing = null; try { root_layer_thing = loader.getRootLayerThing(project, project.root_pt, Project.layer_set_template, Project.layer_template); if (null == root_layer_thing) { project.destroy(); Utils.showMessage("Could not retrieve the root layer thing."); return null; } // set the child/parent relationships now that everything exists root_layer_thing.setup(); project.layer_set = (LayerSet)root_layer_thing.getObject(); if (null == project.layer_set) { project.destroy(); Utils.showMessage("Could not retrieve the root layer set."); return null; } project.layer_set.setup(); // set the active layer to each ZDisplayable // debug: //Utils.log2("$$$ root_lt: " + root_layer_thing + " ob: " + root_layer_thing.getObject().getClass().getName() + "\n children: " + ((LayerSet)root_layer_thing.getObject()).getLayers().size()); project.layer_tree = new LayerTree(project, root_layer_thing); project.root_lt = root_layer_thing; } catch (Exception e) { Utils.showMessage("Failed to retrieve the Layer tree for the project."); IJError.print(e); project.destroy(); return null; } // if all when well, register as open: al_open_projects.add(project); // create the project control window, containing the trees in a double JSplitPane ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree); // now open the displays that were stored for later, if any: Display.openLater(); return project; } /** Creates a new project to be based on .xml and image files, not a database. Images are left where they are, keeping the path to them. If the arg equals 'blank', then no template is asked for. */ static public Project newFSProject(String arg) { return newFSProject(arg, null); } static public Project newFSProject(String arg, TemplateThing template_root) { return newFSProject(arg, null, null); } /** Creates a new project to be based on .xml and image files, not a database. * Images are left where they are, keeping the path to them. * If the arg equals 'blank', then no template is asked for; * if template_root is not null that is used; else, a template file is asked for. * * @param arg Either "blank", "amira", "stack" or null. "blank" will generate a default template tree; "amira" will ask for importing an Amira file; "stack" will ask for importing an image stack (single multi-image file, like multi-TIFF). * @param template_root May be null, in which case a template DTD or XML file will be asked for, unless {@code arg} equals "blank". * @param storage_folder If null, a dialog asks for it. */ static public Project newFSProject(String arg, TemplateThing template_root, String storage_folder) { return newFSProject(arg, template_root, storage_folder, true); } static public Project newFSProject(String arg, TemplateThing template_root, String storage_folder, boolean autocreate_one_layer) { if (Utils.wrongImageJVersion()) return null; FSLoader loader = null; try { String dir_project = storage_folder; if (null == dir_project || !new File(dir_project).isDirectory()) { DirectoryChooser dc = new DirectoryChooser("Select storage folder"); dir_project = dc.getDirectory(); if (null == dir_project) return null; // user cancelled dialog if (!Loader.canReadAndWriteTo(dir_project)) { Utils.showMessage("Can't read/write to the selected storage folder.\nPlease check folder permissions."); return null; } if (IJ.isWindows()) dir_project = dir_project.replace('\\', '/'); } loader = new FSLoader(dir_project); Project project = createNewProject(loader, !("blank".equals(arg) || "amira".equals(arg)), template_root); // help the helpless users: if (autocreate_one_layer && null != project && ControlWindow.isGUIEnabled()) { Utils.log2("Creating automatic Display."); // add a default layer Layer layer = new Layer(project, 0, 1, project.layer_set); project.layer_set.add(layer); project.layer_tree.addLayer(project.layer_set, layer); layer.recreateBuckets(); Display.createDisplay(project, layer); } try { Thread.sleep(200); // waiting cheaply for asynchronous swing calls } catch (InterruptedException ie) { ie.printStackTrace(); } if ("amira".equals(arg) || "stack".equals(arg)) { // forks into a task thread loader.importStack(project.layer_set.getLayer(0), null, true); } project.restartAutosaving(); return project; } catch (Exception e) { IJError.print(e); if (null != loader) loader.destroy(); } return null; } static public Project openFSProject(final String path) { return openFSProject(path, true); } /** Opens a project from an .xml file. If the path is null it'll be asked for. * Only one project may be opened at a time. */ @SuppressWarnings("unchecked") synchronized static public Project openFSProject(final String path, final boolean open_displays) { if (Utils.wrongImageJVersion()) return null; final FSLoader loader = new FSLoader(); final Object[] data = loader.openFSProject(path, open_displays); if (null == data) { loader.destroy(); return null; } final TemplateThing root_tt = (TemplateThing)data[0]; final ProjectThing root_pt = (ProjectThing)data[1]; final LayerThing root_lt = (LayerThing)data[2]; final HashMap<ProjectThing,Boolean> ht_pt_expanded = (HashMap<ProjectThing,Boolean>)data[3]; final Project project = (Project)root_pt.getObject(); project.createLayerTemplates(); project.template_tree = new TemplateTree(project, root_tt); project.root_tt = root_tt; project.root_pt= root_pt; project.project_tree = new ProjectTree(project, project.root_pt); project.layer_tree = new LayerTree(project, root_lt); project.root_lt = root_lt; project.layer_set = (LayerSet)root_lt.getObject(); // if all when well, register as open: al_open_projects.add(project); // create the project control window, containing the trees in a double JSplitPane ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree); // debug: print the entire root project tree //project.root_pt.debug(""); // set ProjectThing nodes expanded state, now that the trees exist try { java.lang.reflect.Field f = JTree.class.getDeclaredField("expandedState"); f.setAccessible(true); Hashtable<Object,Object> ht_exp = (Hashtable<Object,Object>) f.get(project.project_tree); for (Map.Entry<ProjectThing,Boolean> entry : ht_pt_expanded.entrySet()) { ProjectThing pt = entry.getKey(); Boolean expanded = entry.getValue(); //project.project_tree.expandPath(new TreePath(project.project_tree.findNode(pt, project.project_tree).getPath())); // WARNING the above is wrong in that it will expand the whole thing, not just set the state of the node!! // So the ONLY way to do it is to start from the child-most leafs of the tree, and apply the expanding to them upward. This is RIDICULOUS, how can it be so broken // so, hackerous: DefaultMutableTreeNode nd = DNDTree.findNode(pt, project.project_tree); //if (null == nd) Utils.log2("null node for " + pt); //else Utils.log2("path: " + new TreePath(nd.getPath())); if (null == nd) { Utils.log2("Can't find node for " + pt); } else { ht_exp.put(new TreePath(nd.getPath()), expanded); } } project.project_tree.updateUILater(); // very important!! } catch (Exception e) { IJError.print(e); } // open any stored displays if (open_displays) { final Bureaucrat burro = Display.openLater(); if (null != burro) { final Runnable ru = new Runnable() { public void run() { // wait until the Bureaucrat finishes try { burro.join(); } catch (InterruptedException ie) {} // restore to non-changes (crude, but works) project.loader.setChanged(false); Utils.log2("C set to false"); } }; new Thread() { public void run() { setPriority(Thread.NORM_PRIORITY); // avoiding "can't call invokeAndWait from the EventDispatch thread" error try { javax.swing.SwingUtilities.invokeAndWait(ru); } catch (Exception e) { Utils.log2("ERROR: " + e); } } }.start(); // SO: WAIT TILL THE END OF TIME! new Thread() { public void run() { try { Thread.sleep(4000); // ah, the pain in my veins. I can't take this shitty setup anymore. javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { project.getLoader().setChanged(false); Utils.log2("D set to false"); }}); project.getTemplateTree().updateUILater(); // repainting to fix gross errors in tree rendering project.getProjectTree().updateUILater(); // idem } catch (Exception ie) {} }}.start(); } else { // help the helpless users Display.createDisplay(project, project.layer_set.getLayer(0)); } } project.restartAutosaving(); return project; } static private Project createNewProject(Loader loader, boolean ask_for_template) { return createNewProject(loader, ask_for_template, null); } static private Project createNewProject(Loader loader, boolean ask_for_template, TemplateThing template_root) { return createNewProject(loader, ask_for_template, template_root, false); } static private Project createNewProject(Loader loader, boolean ask_for_template, TemplateThing template_root, boolean clone_ids) { Project project = new Project(loader); // ask for an XML properties file that defines the Thing objects that can be created // (the XML file will be parsed into a TemplateTree filled with TemplateThing objects) //Utils.log2("ask_for_template: " + ask_for_template); if (ask_for_template) template_root = project.loader.askForXMLTemplate(project); if (null == template_root) { template_root = new TemplateThing("anything"); } else if (clone_ids) { // the given template_root belongs to another project from which we are cloning template_root = template_root.clone(project, true); } // else, use the given template_root as is. // create tree project.template_tree = new TemplateTree(project, template_root); project.root_tt = template_root; // collect unique TemplateThing instances synchronized (project.ht_unique_tt) { project.ht_unique_tt.clear(); project.ht_unique_tt.putAll(template_root.getUniqueTypes(new HashMap<String,TemplateThing>())); } // add all TemplateThing objects to the database, recursively if (!clone_ids) template_root.addToDatabase(project); // else already done when cloning the root_tt // create a non-database bound template for the project Thing TemplateThing project_template = new TemplateThing("project"); project.ht_unique_tt.put("project", project_template); project_template.addChild(template_root); // create the project Thing, to be root of the whole project thing tree try { project.root_pt= new ProjectThing(project_template, project, project); } catch (Exception e) { IJError.print(e); } // create the user objects tree project.project_tree = new ProjectTree(project, project.root_pt); // create the layer's tree project.createLayerTemplates(); project.layer_set = new LayerSet(project, "Top Level", 0, 0, null, 2048, 2048); // initialized with default values, and null parent to signal 'root' try { project.root_lt = new LayerThing(Project.layer_set_template, project, project.layer_set); project.layer_tree = new LayerTree(project, project.root_lt); } catch (Exception e) { project.remove(); IJError.print(e); } // create the project control window, containing the trees in a double JSplitPane ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree); // beware that this call is asynchronous, dispatched by the SwingUtilities.invokeLater to avoid havok with Swing components. // register al_open_projects.add(project); return project; } public void setTempLoader(Loader loader) { if (null == this.loader) { this.loader = loader; } else { Utils.log2("Project.setTempLoader: already have one."); } } public final Loader getLoader() { return loader; } /** Save the project regardless of what getLoader().hasChanges() reports. */ public String save() { Thread.yield(); // let it repaint the log window XMLOptions options = new XMLOptions(); options.overwriteXMLFile = true; options.export_images = false; options.patches_dir = null; options.include_coordinate_transform = true; String path = loader.save(this, options); if (null != path) restartAutosaving(); return path; } /** This is not the saveAs used from the menus; this one is meant for programmatic access. */ public String saveAs(String xml_path, boolean overwrite) throws IllegalArgumentException { if (null == xml_path) throw new IllegalArgumentException("xml_path cannot be null."); XMLOptions options = new XMLOptions(); options.overwriteXMLFile = overwrite; options.export_images = false; options.patches_dir = null; options.include_coordinate_transform = true; String path = loader.saveAs(xml_path, options); if (null != path) restartAutosaving(); return path; } /** Save an XML file that is stripped of coordinate transforms, * and merely refers to them by the 'ct_id' attribute of each 't2_patch' element; * this method will NOT overwrite the XML file but save into a new one, * which is chosen from a file dialog. */ public String saveWithoutCoordinateTransforms() { XMLOptions options = new XMLOptions(); options.overwriteXMLFile = false; options.export_images = false; options.include_coordinate_transform = false; options.patches_dir = null; return loader.saveAs(this, options); } public boolean destroy() { if (null == loader) { return true; } if (loader.hasChanges() && !getBooleanProperty("no_shutdown_hook")) { // DBLoader always returns false if (ControlWindow.isGUIEnabled()) { final YesNoDialog yn = ControlWindow.makeYesNoDialog("TrakEM2", "There are unsaved changes in project " + title + ". Save them?"); if (yn.yesPressed()) { save(); } } else { Utils.log2("WARNING: closing project '" + title + "' with unsaved changes."); } } try { if (null != autosaving) autosaving.cancel(true); } catch (Throwable t) {} al_open_projects.remove(this); // flush all memory if (null != loader) { // the last project is destroyed twice for some reason, if several are open. This is a PATCH loader.destroy(); // and disconnect loader = null; } if (null != layer_set) layer_set.destroy(); ControlWindow.remove(this); // AFTER loader.destroy() call. if (null != template_tree) template_tree.destroy(); if (null != project_tree) project_tree.destroy(); if (null != layer_tree) layer_tree.destroy(); Polyline.flushTraceCache(this); this.template_tree = null; // flag to mean: we're closing // close all open Displays Display.close(this); Search.removeTabs(this); synchronized (ptcache) { ptcache.clear(); } return true; } public boolean isBeingDestroyed() { return null == template_tree; } /** Remove the project from the database and release memory. */ public void remove() { removeFromDatabase(); destroy(); } /** Remove the project from the database and release memory. */ public boolean remove(boolean check) { if (!Utils.check("Delete the project " + toString() + " from the database?")) return false; removeFromDatabase(); destroy(); return true; } public void setTitle(String title) { if (null == title) return; this.title = title; ControlWindow.updateTitle(this); loader.updateInDatabase(this, "title"); } public String toString() { if (null == title || title.equals("Project")) { try { return loader.makeProjectName(); // can't use this.id, because the id system is project-centric and thus all FSLoader projects would have the same id. } catch (Exception e) { Utils.log2("Swing again."); } } return title; } public String getTitle() { return title; } public TemplateTree getTemplateTree() { return template_tree; } public LayerTree getLayerTree() { return layer_tree; } public ProjectTree getProjectTree() { return project_tree; } /** Make an object of the type the TemplateThing can hold. */ public Object makeObject(final TemplateThing tt) { final String type = tt.getType(); if (type.equals("profile")) { ProjectToolbar.setTool(ProjectToolbar.PENCIL); // this should go elsewhere, in display issues. return new Profile(this, "profile", 0, 0); } else if (type.equals("pipe")) { ProjectToolbar.setTool(ProjectToolbar.PEN); return new Pipe(this, "pipe", 0, 0); } else if (type.equals("polyline")) { ProjectToolbar.setTool(ProjectToolbar.PEN); return new Polyline(this, "polyline"); } else if (type.equals("area_list")) { ProjectToolbar.setTool(ProjectToolbar.BRUSH); return new AreaList(this, "area_list", 0, 0); } else if (type.equals("treeline")) { ProjectToolbar.setTool(ProjectToolbar.PEN); return new Treeline(this, "treeline"); } else if (type.equals("areatree")) { ProjectToolbar.setTool(ProjectToolbar.PEN); return new AreaTree(this, "areatree"); } else if (type.equals("ball")) { ProjectToolbar.setTool(ProjectToolbar.PEN); return new Ball(this, "ball", 0, 0); } else if (type.equals("connector")) { ProjectToolbar.setTool(ProjectToolbar.PEN); return new Connector(this, "connector"); } else if (type.equals("dissector")) { ProjectToolbar.setTool(ProjectToolbar.PEN); return new Dissector(this, "dissector", 0, 0); } else if (type.equals("label")) { return new DLabel(this, " ", 0, 0); // never used so far } else { // just the name, for the abstract ones return type; } } /** Returns true if the type is 'patch', 'layer', 'layer_set', 'profile', 'profile_list' 'pipe'. */ static public boolean isBasicType(final String type) { return isProjectType(type) || isLayerSetType(type) || isLayerType(type) ; } static public boolean isProjectType(String type) { type = type.toLowerCase(); return type.equals("profile_list"); } static public boolean isLayerSetType(String type) { type = type.toLowerCase().replace(' ', '_'); return type.equals("area_list") || type.equals("pipe") || type.equals("ball") || type.equals("polyline") || type.equals("dissector") || type.equals("stack") || type.equals("treeline") || type.equals("areatree") || type.equals("connector") ; } static public boolean isLayerType(String type) { type = type.toLowerCase().replace(' ', '_'); return type.equals("patch") || type.equals("profile") || type.equals("layer") || type.equals("layer_set") // for XML || type.equals("label") ; } /** Remove the ProjectThing that contains the given object, which will remove the object itself as well. */ public boolean removeProjectThing(Object object, boolean check) { return removeProjectThing(object, check, false, 0); } /** Remove the ProjectThing that contains the given object, which will remove the object itself as well. */ public boolean removeProjectThing(Object object, boolean check, boolean remove_empty_parents, int levels) { if (levels < 0) { Utils.log2("Project.removeProjectThing: levels must be zero or above."); return false; } // find the Thing DefaultMutableTreeNode root = (DefaultMutableTreeNode)project_tree.getModel().getRoot(); Enumeration<?> e = root.depthFirstEnumeration(); DefaultMutableTreeNode node = null; while (e.hasMoreElements()) { node = (DefaultMutableTreeNode)e.nextElement(); Object ob = node.getUserObject(); if (ob instanceof ProjectThing && ((ProjectThing)ob).getObject() == object) { if (check && !Utils.check("Remove " + object.toString() + "?")) return false; // remove the ProjectThing, its object and the node that holds it. project_tree.remove(node, false, remove_empty_parents, levels); return true; } // the above could be done more generic with a Thing.contains(Object), but I want to make sure that the object is contained by a ProjectThing and nothing else. } // not found: return false; } /** Find the node in the layer tree with a Thing that contains the given object, and set it selected/highlighted, deselecting everything else first. */ public void select(final Layer layer) { layer_tree.selectNode(layer); } /** Find the node in any tree with a Thing that contains the given Displayable, and set it selected/highlighted, deselecting everything else first. */ public void select(final Displayable d) { if (d.getClass() == LayerSet.class) select(d, layer_tree); else { ProjectThing pt = findProjectThing(d); // from cache: one linear search less if (null != pt) DNDTree.selectNode(pt, project_tree); } } private final void select(final Object ob, final DNDTree tree) { // Find the Thing that contains the object final Thing root_thing = (Thing)((DefaultMutableTreeNode)tree.getModel().getRoot()).getUserObject(); final Thing child_thing = root_thing.findChild(ob); // find the node that contains the Thing, and select it DNDTree.selectNode(child_thing, tree); } /** Find the ProjectThing instance with the given id. */ public ProjectThing find(final long id) { // can't be the Project itself return root_pt.findChild(id); } public DBObject findById(final long id) { if (this.id == id) return this; DBObject dbo = layer_set.findById(id); if (null != dbo) return dbo; dbo = root_pt.findChild(id); // could call findObject(id), but all objects must exist in layer sets anyway. if (null != dbo) return dbo; return (DBObject)root_tt.findChild(id); } /** Find a LayerThing that contains the given object. */ public LayerThing findLayerThing(final Object ob) { final Object lob = root_lt.findChild(ob); return null != lob ? (LayerThing)lob : null; } private final Map<Object,ProjectThing> ptcache = new HashMap<Object, ProjectThing>(); /** Find a ProjectThing that contains the given object. */ public ProjectThing findProjectThing(final Object ob) { ProjectThing pt; synchronized (ptcache) { pt = ptcache.get(ob); } if (null == pt) { pt = (ProjectThing) root_pt.findChild(ob); if (null != ob) synchronized (ptcache) { ptcache.put(ob, pt); } } return pt; } public void decache(final Object ob) { synchronized (ptcache) { ptcache.remove(ob); } } public ProjectThing getRootProjectThing() { return root_pt; } public LayerSet getRootLayerSet() { return layer_set; } /** Returns the title of the enclosing abstract node in the ProjectTree.*/ public String getParentTitle(final Displayable d) { try { ProjectThing thing = findProjectThing(d); ProjectThing parent = (ProjectThing)thing.getParent(); if (d instanceof Profile) { parent = (ProjectThing)parent.getParent(); // skip the profile_list } if (null == parent) Utils.log2("null parent for " + d); if (null != parent && null == parent.getObject()) { Utils.log2("null ob for parent " + parent + " of " + d); } return parent.getObject().toString(); // the abstract thing should be enclosing a String object } catch (Exception e) { IJError.print(e); return null; } } public String getMeaningfulTitle2(final Displayable d) { final ProjectThing thing = findProjectThing(d); if (null == thing) return d.getTitle(); // happens if there is no associated node if (!thing.getType().equals(d.getTitle())) { return new StringBuilder(!thing.getType().equals(d.getTitle()) ? d.getTitle() + " [" : "[").append(thing.getType()).append(']').toString(); } // Else, search upstream for a ProjectThing whose name differs from its type Thing parent = (ProjectThing)thing.getParent(); while (null != parent) { String type = parent.getType(); Object ob = parent.getObject(); if (ob.getClass() == Project.class) break; if (!ob.equals(type)) { return ob.toString() + " [" + thing.getType() + "]"; } parent = parent.getParent(); } if (d.getTitle().equals(thing.getType())) return "[" + thing.getType() + "]"; return d.getTitle() + " [" + thing.getType() + "]"; } /** Searches upstream in the Project tree for things that have a user-defined name, stops at the first and returns it along with all the intermediate ones that only have a type and not a title, appended. */ public String getMeaningfulTitle(final Displayable d) { ProjectThing thing = findProjectThing(d); if (null == thing) return d.getTitle(); // happens if there is no associated node String title = new StringBuilder(!thing.getType().equals(d.getTitle()) ? d.getTitle() + " [" : "[").append(thing.getType()).append(' ').append('#').append(d.getId()).append(']').toString(); if (!thing.getType().equals(d.getTitle())) { return title; } ProjectThing parent = (ProjectThing)thing.getParent(); StringBuilder sb = new StringBuilder(title); while (null != parent) { Object ob = parent.getObject(); if (ob.getClass() == Project.class) break; String type = parent.getType(); if (!ob.equals(type)) { // meaning, something else was typed in as a title sb.insert(0, new StringBuilder(ob.toString()).append(' ').append('[').append(type).append(']').append('/').toString()); //title = ob.toString() + " [" + type + "]/" + title; break; } sb.insert(0, '/'); sb.insert(0, type); //title = type + "/" + title; parent = (ProjectThing)parent.getParent(); } //return title; return sb.toString(); } /** Returns the first upstream user-defined name and type, and the id of the displayable tagged at the end. * If no user-defined name is found, then the type is prepended to the id. */ public String getShortMeaningfulTitle(final Displayable d) { ProjectThing thing = findProjectThing(d); if (null == thing) return d.getTitle(); // happens if there is no associated node return getShortMeaningfulTitle(thing, d); } public String getShortMeaningfulTitle(final ProjectThing thing, final Displayable d) { if (thing.getObject() != d) { return thing.toString(); } ProjectThing parent = (ProjectThing)thing.getParent(); String title = "#" + d.getId(); while (null != parent) { Object ob = parent.getObject(); String type = parent.getType(); if (!ob.equals(type)) { // meaning, something else was typed in as a title title = ob.toString() + " [" + type + "] " + title; break; } parent = (ProjectThing)parent.getParent(); } // if nothing found, prepend the type if ('#' == title.charAt(0)) title = Project.getName(d.getClass()) + " " + title; return title; } static public String getType(final Class<?> c) { if (AreaList.class == c) return "area_list"; if (DLabel.class == c) return "label"; String name = c.getName().toLowerCase(); int i = name.lastIndexOf('.'); if (-1 != i) name = name.substring(i+1); return name; } /** Returns the proper TemplateThing for the given type, complete with children and attributes if any. */ public TemplateThing getTemplateThing(String type) { return ht_unique_tt.get(type); } /** Returns a list of existing unique types in the template tree * (thus the 'project' type is not included, nor the label). * The basic types are guaranteed to be present even if there are no instances in the template tree. * As a side effect, this method populates the HashMap of unique TemplateThing types. */ public String[] getUniqueTypes() { synchronized (ht_unique_tt) { // ensure the basic types (pipe, ball, profile, profile_list) are present if (!ht_unique_tt.containsKey("profile")) ht_unique_tt.put("profile", new TemplateThing("profile")); if (!ht_unique_tt.containsKey("profile_list")) { TemplateThing tpl = new TemplateThing("profile_list"); tpl.addChild((TemplateThing) ht_unique_tt.get("profile")); ht_unique_tt.put("profile_list", tpl); } if (!ht_unique_tt.containsKey("pipe")) ht_unique_tt.put("pipe", new TemplateThing("pipe")); if (!ht_unique_tt.containsKey("polyline")) ht_unique_tt.put("polyline", new TemplateThing("polyline")); if (!ht_unique_tt.containsKey("treeline")) ht_unique_tt.put("treeline", new TemplateThing("treeline")); if (!ht_unique_tt.containsKey("areatree")) ht_unique_tt.put("areatree", new TemplateThing("areatree")); if (!ht_unique_tt.containsKey("connector")) ht_unique_tt.put("connector", new TemplateThing("connector")); if (!ht_unique_tt.containsKey("ball")) ht_unique_tt.put("ball", new TemplateThing("ball")); if (!ht_unique_tt.containsKey("area_list")) ht_unique_tt.put("area_list", new TemplateThing("area_list")); if (!ht_unique_tt.containsKey("dissector")) ht_unique_tt.put("dissector", new TemplateThing("dissector")); // this should be done automagically by querying the classes in the package ... but java can't do that without peeking into the .jar .class files. Buh. TemplateThing project_tt = ht_unique_tt.remove("project"); /* // debug for (Iterator it = ht_unique_tt.keySet().iterator(); it.hasNext(); ) { Utils.log2("class: " + it.next().getClass().getName()); } */ final String[] ut = new String[ht_unique_tt.size()]; ht_unique_tt.keySet().toArray(ut); ht_unique_tt.put("project", project_tt); Arrays.sort(ut); return ut; } } /** Remove a unique type from the HashMap. Basic types can't be removed. */ public boolean removeUniqueType(String type) { if (null == type || isBasicType(type)) return false; synchronized (ht_unique_tt) { return null != ht_unique_tt.remove(type); } } public boolean typeExists(String type) { return ht_unique_tt.containsKey(type); } /** Returns false if the type exists already. */ public boolean addUniqueType(TemplateThing tt) { synchronized (ht_unique_tt) { if (ht_unique_tt.containsKey(tt.getType())) return false; ht_unique_tt.put(tt.getType(), tt); } return true; } public boolean updateTypeName(String old_type, String new_type) { synchronized (ht_unique_tt) { if (ht_unique_tt.containsKey(new_type)) { Utils.showMessage("Can't rename type '" + old_type + "' : a type named '"+new_type+"' already exists!"); return false; } ht_unique_tt.put(new_type, ht_unique_tt.remove(old_type)); return true; } } private void createLayerTemplates() { if (null == layer_template) { layer_template = new TemplateThing("layer"); layer_set_template = new TemplateThing("layer_set"); layer_set_template.addChild(layer_template); layer_template.addChild(layer_set_template); // adding a new instance to keep parent/child relationships clean // No need, there won't ever be a loop so far WARNING may change in the future. } } @Override public void exportXML(final StringBuilder sb, final String indent, final XMLOptions options) { Utils.logAll("ERROR: cannot call Project.exportXML(StringBuilder, String, ExportOptions) !!"); throw new UnsupportedOperationException("Cannot call Project.exportXML(StringBuilder, String, Object)"); } /** Export the main trakem2 tag wrapping four hierarchies (the project tag, the ProjectTree, and the Top Level LayerSet the latter including all Displayable objects) and a list of displays. */ public void exportXML(final java.io.Writer writer, final String indent, final XMLOptions options) throws Exception { Utils.showProgress(0); // 1 - opening tag writer.write(indent); writer.write("<trakem2>\n"); final String in = indent + "\t"; // 2,3 - export the project itself exportXML2(writer, in, options); // 4 - export LayerSet hierarchy of Layer, LayerSet and Displayable objects layer_set.exportXML(writer, in, options); // 5 - export Display objects Display.exportXML(this, writer, in, options); // 6 - closing tag writer.write("</trakem2>\n"); } // A separate method to ensure that sb_body instance is garbage collected. private final void exportXML2(final java.io.Writer writer, final String in, final XMLOptions options) throws Exception { final StringBuilder sb_body = new StringBuilder(); // 2 - the project itself sb_body.append(in).append("<project \n") .append(in).append("\tid=\"").append(id).append("\"\n") .append(in).append("\ttitle=\"").append(title).append("\"\n"); loader.insertXMLOptions(sb_body, in + "\t"); // Write properties, with the additional property of the image_resizing_mode final HashMap<String,String> props = new HashMap<String, String>(ht_props); props.put("image_resizing_mode", Loader.getMipMapModeName(mipmaps_mode)); for (final Map.Entry<String, String> e : props.entrySet()) { sb_body.append(in).append('\t').append(e.getKey()).append("=\"").append(e.getValue()).append("\"\n"); } sb_body.append(in).append(">\n"); // 3 - export ProjectTree abstract hierarchy (skip the root since it wraps the project itself) project_tree.getExpandedStates(options.expanded_states); if (null != root_pt.getChildren()) { final String in2 = in + "\t"; for (final ProjectThing pt : root_pt.getChildren()) { pt.exportXML(sb_body, in2, options); } } sb_body.append(in).append("</project>\n"); writer.write(sb_body.toString()); } /** Export a complete DTD listing to export the project as XML. */ public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) { // 1 - TrakEM2 tag that encloses all hierarchies sb_header.append(indent).append("<!ELEMENT ").append("trakem2 (project,t2_layer_set,t2_display)>\n"); // 2 - export user-defined templates //TemplateThing root_tt = (TemplateThing)((DefaultMutableTreeNode)((DefaultTreeModel)template_tree.getModel()).getRoot()).getUserObject(); sb_header.append(indent).append("<!ELEMENT ").append("project (").append(root_tt.getType()).append(")>\n"); sb_header.append(indent).append("<!ATTLIST project id NMTOKEN #REQUIRED>\n"); sb_header.append(indent).append("<!ATTLIST project unuid NMTOKEN #REQUIRED>\n"); sb_header.append(indent).append("<!ATTLIST project title NMTOKEN #REQUIRED>\n"); sb_header.append(indent).append("<!ATTLIST project preprocessor NMTOKEN #REQUIRED>\n"); sb_header.append(indent).append("<!ATTLIST project mipmaps_folder NMTOKEN #REQUIRED>\n"); sb_header.append(indent).append("<!ATTLIST project storage_folder NMTOKEN #REQUIRED>\n"); for (String key : ht_props.keySet()) { sb_header.append(indent).append("<!ATTLIST project ").append(key).append(" NMTOKEN #REQUIRED>\n"); } root_tt.exportDTD(sb_header, hs, indent); // 3 - export all project objects DTD in the Top Level LayerSet Layer.exportDTD(sb_header, hs, indent); LayerSet.exportDTD(sb_header, hs, indent); Ball.exportDTD(sb_header, hs, indent); DLabel.exportDTD(sb_header, hs, indent); Patch.exportDTD(sb_header, hs, indent); Pipe.exportDTD(sb_header, hs, indent); Polyline.exportDTD(sb_header, hs, indent); Profile.exportDTD(sb_header, hs, indent); AreaList.exportDTD(sb_header, hs, indent); Dissector.exportDTD(sb_header, hs, indent); Stack.exportDTD( sb_header, hs, indent ); Treeline.exportDTD(sb_header, hs, indent); AreaTree.exportDTD(sb_header, hs, indent); Connector.exportDTD(sb_header, hs, indent); Displayable.exportDTD(sb_header, hs, indent); // the subtypes of all Displayable types // 4 - export Display Display.exportDTD(sb_header, hs, indent); // all the above could be done with reflection, automatically detecting the presence of an exportDTD method. // CoordinateTransforms mpicbg.trakem2.transform.DTD.append( sb_header, hs, indent ); } /** Returns the String to be used as Document Type of the XML file, generated from the name of the root template thing.*/ public String getDocType() { //TemplateThing root_tt = (TemplateThing)((DefaultMutableTreeNode)((DefaultTreeModel)template_tree.getModel()).getRoot()).getUserObject(); return "trakem2_" + root_tt.getType(); } /** Returns a user-understandable name for the given class. */ static public String getName(final Class<?> c) { String name = c.getName(); name = name.substring(name.lastIndexOf('.') + 1); if (name.equals("DLabel")) return "Label"; else if (name.equals("Patch")) return "Image"; //else if (name.equals("Pipe")) return "Tube"; //else if (name.equals("Ball")) return "Sphere group"; // TODO revise consistency with XML templates and so on else return name; } public String getInfo() { StringBuilder sb = new StringBuilder("Project id: "); sb.append(this.id).append("\nProject name: ").append(this.title) .append("\nTrees:\n") .append(project_tree.getInfo()).append("\n") .append(layer_tree.getInfo()) ; return sb.toString(); } static public Project findProject(Loader loader) { for (final Project pro : al_open_projects) { if (pro.getLoader() == loader) return pro; } return null; } private boolean input_disabled = false; /** Tells the displays concerning this Project to accept/reject input. */ public void setReceivesInput(boolean b) { this.input_disabled = !b; Display.setReceivesInput(this, b); } public boolean isInputEnabled() { return !input_disabled; } /** Create a new subproject for the given layer range and ROI. * Create a new Project using the given project as template. This means the DTD of the given project is copied, as well as the storage and mipmaps folders; everything else is empty in the new project. */ public Project createSubproject(final Rectangle roi, final Layer first, final Layer last, final boolean ignore_hidden_patches) { try { // The order matters. final Project pr = new Project(new FSLoader(this.getLoader().getStorageFolder())); pr.id = this.id; // copy properties pr.title = this.title; pr.ht_props.putAll(this.ht_props); // copy template pr.root_tt = this.root_tt.clone(pr, true); pr.template_tree = new TemplateTree(pr, pr.root_tt); synchronized (pr.ht_unique_tt) { pr.ht_unique_tt.clear(); pr.ht_unique_tt.putAll(root_tt.getUniqueTypes(new HashMap<String,TemplateThing>())); } TemplateThing project_template = new TemplateThing("project"); project_template.addChild(pr.root_tt); pr.ht_unique_tt.put("project", project_template); // create the layers templates pr.createLayerTemplates(); // copy LayerSet and all involved Displayable objects // (A two-step process to provide the layer_set pointer and all Layer pointers to the ZDisplayable to copy and crop.) pr.layer_set = (LayerSet)this.layer_set.clone(pr, first, last, roi, false, true, ignore_hidden_patches); LayerSet.cloneInto(this.layer_set, first, last, pr, pr.layer_set, roi, true); // create layer tree pr.root_lt = new LayerThing(Project.layer_set_template, pr, pr.layer_set); pr.layer_tree = new LayerTree(pr, pr.root_lt); // add layer nodes to the layer tree (solving chicken-and-egg problem) pr.layer_set.updateLayerTree(); // copy project tree pr.root_pt = this.root_pt.subclone(pr); pr.project_tree = new ProjectTree(pr, pr.root_pt); // not copying node expanded state. // register al_open_projects.add(pr); // add to gui: ControlWindow.add(pr, pr.template_tree, pr.project_tree, pr.layer_tree); // Above, the id of each object is preserved from this project into the subproject. // The abstract structure should be copied in full regardless, without the basic objects // included if they intersect the roi. // Regenerate mipmaps (blocks GUI from interaction other than navigation) pr.loader.regenerateMipMaps(pr.layer_set.getDisplayables(Patch.class)); pr.restartAutosaving(); return pr; } catch (Exception e) { e.printStackTrace(); } return null; } public void parseXMLOptions(final HashMap<String,String> ht_attributes) { ((FSLoader)this.project.getLoader()).parseXMLOptions(ht_attributes); // String mipmapsMode = ht_attributes.remove("image_resizing_mode"); this.mipmaps_mode = null == mipmapsMode ? Loader.DEFAULT_MIPMAPS_MODE : Loader.getMipMapModeIndex(mipmapsMode); // // all keys that remain are properties ht_props.putAll(ht_attributes); for (Map.Entry<String,String> prop : ht_attributes.entrySet()) { Utils.log2("parsed: " + prop.getKey() + "=" + prop.getValue()); } } public HashMap<String,String> getPropertiesCopy() { return new HashMap<String,String>(ht_props); } /** Returns null if not defined. */ public String getProperty(final String key) { return ht_props.get(key); } /** Returns the default value if not defined, or if not a number or not parsable as a number. */ public float getProperty(final String key, final float default_value) { try { final String s = ht_props.get(key); if (null == s) return default_value; final float num = Float.parseFloat(s); if (Float.isNaN(num)) return default_value; return num; } catch (NumberFormatException nfe) { IJError.print(nfe); } return default_value; } public int getProperty(final String key, final int default_value) { try { final String s = ht_props.get(key); if (null == s) return default_value; return Integer.parseInt(s); } catch (NumberFormatException nfe) { IJError.print(nfe); } return default_value; } public boolean getBooleanProperty(final String key) { return "true".equals(ht_props.get(key)); } public void setProperty(final String key, final String value) { if (null == value) ht_props.remove(key); else ht_props.put(key, value); } private final boolean addBox(final GenericDialog gd, final Class<?> c) { final String name = Project.getName(c); final boolean link = "true".equals(ht_props.get(name.toLowerCase() + "_nolinks")); gd.addCheckbox(name, link); return link; } private final void setLinkProp(final boolean before, final boolean after, final Class<?> c) { if (before) { if (!after) ht_props.remove(Project.getName(c).toLowerCase()+"_nolinks"); } else if (after) { ht_props.put(Project.getName(c).toLowerCase()+"_nolinks", "true"); } // setting to false would have no meaning, so the link prop is removed } /** Returns true if there were any changes. */ private final boolean adjustProp(final String prop, final boolean before, final boolean after) { if (before) { if (!after) ht_props.remove(prop); } else if (after) { ht_props.put(prop, "true"); } return before != after; } public void adjustProperties() { // should be more generic, but for now it'll do GenericDialog gd = new GenericDialog("Properties"); gd.addMessage("Ignore image linking for:"); boolean link_labels = addBox(gd, DLabel.class); boolean nolink_segmentations = "true".equals(ht_props.get("segmentations_nolinks")); gd.addCheckbox("Segmentations", nolink_segmentations); gd.addMessage("Currently linked objects will remain so\nunless explicitly unlinked."); boolean dissector_zoom = "true".equals(ht_props.get("dissector_zoom")); gd.addCheckbox("Zoom-invariant markers for Dissector", dissector_zoom); gd.addChoice("Image_resizing_mode: ", Loader.MIPMAP_MODES.values().toArray(new String[Loader.MIPMAP_MODES.size()]), Loader.getMipMapModeName(mipmaps_mode)); gd.addChoice("mipmaps format:", FSLoader.MIPMAP_FORMATS, FSLoader.MIPMAP_FORMATS[loader.getMipMapFormat()]); boolean layer_mipmaps = "true".equals(ht_props.get("layer_mipmaps")); gd.addCheckbox("Layer_mipmaps", layer_mipmaps); boolean keep_mipmaps = "true".equals(ht_props.get("keep_mipmaps")); gd.addCheckbox("Keep_mipmaps_when_deleting_images", keep_mipmaps); // coping with the fact that thee is no Action context ... there should be one in the Worker thread. int bucket_side = (int)getProperty("bucket_side", Bucket.MIN_BUCKET_SIZE); gd.addNumericField("Bucket side length: ", bucket_side, 0, 6, "pixels"); boolean no_shutdown_hook = "true".equals(ht_props.get("no_shutdown_hook")); gd.addCheckbox("No_shutdown_hook to save the project", no_shutdown_hook); int n_undo_steps = getProperty("n_undo_steps", 32); gd.addSlider("Undo steps", 32, 200, n_undo_steps); boolean flood_fill_to_image_edge = "true".equals(ht_props.get("flood_fill_to_image_edge")); gd.addCheckbox("AreaList_flood_fill_to_image_edges", flood_fill_to_image_edge); int look_ahead_cache = (int)getProperty("look_ahead_cache", 0); gd.addNumericField("Look_ahead_cache:", look_ahead_cache, 0, 6, "layers"); int autosaving_interval = getProperty("autosaving_interval", 10); // default: every 10 minutes gd.addNumericField("Autosave every:", autosaving_interval, 0, 6, "minutes"); int n_mipmap_threads = getProperty("n_mipmap_threads", 1); gd.addSlider("Number of threads for mipmaps", 1, n_mipmap_threads, n_mipmap_threads); int meshResolution = getProperty("mesh_resolution", 32); gd.addSlider("Default mesh resolution for images", 1, 512, meshResolution); // gd.showDialog(); // if (gd.wasCanceled()) return; setLinkProp(link_labels, gd.getNextBoolean(), DLabel.class); boolean nolink_segmentations2 = gd.getNextBoolean(); if (nolink_segmentations) { if (!nolink_segmentations2) ht_props.remove("segmentations_nolinks"); } else if (nolink_segmentations2) ht_props.put("segmentations_nolinks", "true"); if (adjustProp("dissector_zoom", dissector_zoom, gd.getNextBoolean())) { Display.repaint(layer_set); // TODO: should repaint nested LayerSets as well } this.mipmaps_mode = Loader.getMipMapModeIndex(gd.getNextChoice()); final int new_mipmap_format = gd.getNextChoiceIndex(); final int old_mipmap_format = loader.getMipMapFormat(); if (new_mipmap_format != old_mipmap_format) { YesNoDialog yn = new YesNoDialog("MipMaps format", "Changing mipmaps format to '" + FSLoader.MIPMAP_FORMATS[new_mipmap_format] + "'requires regenerating all mipmaps. Proceed?"); if (yn.yesPressed()) { if (loader.setMipMapFormat(new_mipmap_format)) { loader.updateMipMapsFormat(old_mipmap_format, new_mipmap_format); } } } boolean layer_mipmaps2 = gd.getNextBoolean(); if (adjustProp("layer_mipmaps", layer_mipmaps, layer_mipmaps2)) { if (layer_mipmaps && !layer_mipmaps2) { // TODO // 1 - ask first // 2 - remove all existing images from layer.mipmaps folder } else if (!layer_mipmaps && layer_mipmaps2) { // TODO // 1 - ask first // 2 - create de novo all layer mipmaps in a background task } } adjustProp("keep_mipmaps", keep_mipmaps, gd.getNextBoolean()); Utils.log2("keep_mipmaps: " + getBooleanProperty("keep_mipmaps")); // bucket_side = (int)gd.getNextNumber(); if (bucket_side > Bucket.MIN_BUCKET_SIZE) { setProperty("bucket_side", Integer.toString(bucket_side)); layer_set.recreateBuckets(true); } adjustProp("no_shutdown_hook", no_shutdown_hook, gd.getNextBoolean()); n_undo_steps = (int)gd.getNextNumber(); if (n_undo_steps < 0) n_undo_steps = 0; setProperty("n_undo_steps", Integer.toString(n_undo_steps)); adjustProp("flood_fill_to_image_edge", flood_fill_to_image_edge, gd.getNextBoolean()); double d_look_ahead_cache = gd.getNextNumber(); if (!Double.isNaN(d_look_ahead_cache) && d_look_ahead_cache >= 0) { setProperty("look_ahead_cache", Integer.toString((int)d_look_ahead_cache)); if (0 == d_look_ahead_cache) { Display.clearColumnScreenshots(this.layer_set); } else { Utils.logAll("WARNING: look-ahead cache is incomplete.\n Expect issues when editing objects, adding new ones, and the like.\n Use \"Project - Flush image cache\" to fix any lack of refreshing issues you encounter."); } } else { Utils.log2("Ignoring invalid 'look ahead cache' value " + d_look_ahead_cache); } double autosaving_interval2 = gd.getNextNumber(); if (((int)(autosaving_interval2)) == autosaving_interval) { // do nothing } else if (autosaving_interval2 < 0 || Double.isNaN(autosaving_interval)) { Utils.log("IGNORING invalid autosaving interval: " + autosaving_interval2); } else { setProperty("autosaving_interval", Integer.toString((int)autosaving_interval2)); restartAutosaving(); } int n_mipmap_threads2 = (int)Math.max(1, gd.getNextNumber()); if (n_mipmap_threads != n_mipmap_threads2) { setProperty("n_mipmap_threads", Integer.toString(n_mipmap_threads2)); // WARNING: this does it for a static service, affecting all projects! FSLoader.restartMipMapThreads(n_mipmap_threads2); } int meshResolution2 = (int)gd.getNextNumber(); if (meshResolution != meshResolution2) { if (meshResolution2 > 0) { setProperty("mesh_resolution", Integer.toString(meshResolution2)); } else { Utils.log("WARNING: ignoring invalid mesh resolution value " + meshResolution2); } } } /** Return the Universal Near-Unique Id of this project, which may be null for non-FSLoader projects. */ public String getUNUId() { return loader.getUNUId(); } /** Removes an object from this Project. */ public final boolean remove(final Displayable d) { final Set<Displayable> s = new HashSet<Displayable>(); s.add(d); return removeAll(s); } /** Calls Project.removeAll(col, null) */ public final boolean removeAll(final Set<Displayable> col) { return removeAll(col, null); } /** Remove any set of Displayable objects from the Layer, LayerSet and Project Tree as necessary. * ASSUMES there aren't any nested LayerSet objects in @param col. */ public final boolean removeAll(final Set<Displayable> col, final DefaultMutableTreeNode top_node) { // 0. Sort into Displayable and ZDisplayable final Set<ZDisplayable> zds = new HashSet<ZDisplayable>(); final List<Displayable> ds = new ArrayList<Displayable>(); for (final Displayable d : col) { if (d instanceof ZDisplayable) { zds.add((ZDisplayable)d); } else { ds.add(d); } } // Displayable: // 1. First the Profile from the Project Tree, one by one, // while creating a map of Layer vs Displayable list to remove in that layer: final HashMap<Layer,Set<Displayable>> ml = new HashMap<Layer,Set<Displayable>>(); for (final Iterator<Displayable> it = ds.iterator(); it.hasNext(); ) { final Displayable d = it.next(); if (d.getClass() == Profile.class) { if (!project_tree.remove(false, findProjectThing(d), null)) { // like Profile.remove2 Utils.log("Could NOT delete " + d); continue; } it.remove(); // remove the Profile continue; } // The map of Layer vs Displayable list Set<Displayable> l = ml.get(d.getLayer()); if (null == l) { l = new HashSet<Displayable>(); ml.put(d.getLayer(), l); } l.add(d); } // 2. Then the rest, in bulk: if (ml.size() > 0) { for (final Map.Entry<Layer,Set<Displayable>> e : ml.entrySet()) { e.getKey().removeAll(e.getValue()); } } // 3. Stacks if (zds.size() > 0) { final Set<ZDisplayable> stacks = new HashSet<ZDisplayable>(); for (final Iterator<ZDisplayable> it = zds.iterator(); it.hasNext(); ) { final ZDisplayable zd = it.next(); if (zd.getClass() == Stack.class) { it.remove(); stacks.add(zd); } } layer_set.removeAll(stacks); } // 4. ZDisplayable: bulk removal if (zds.size() > 0) { // 1. From the Project Tree: Set<Displayable> not_removed = project_tree.remove(zds, top_node); // 2. Then only those successfully removed, from the LayerSet: zds.removeAll(not_removed); layer_set.removeAll(zds); } // TODO return true; } /** For undo purposes. */ public void resetRootProjectThing(final ProjectThing pt, final HashMap<Thing,Boolean> ptree_exp) { this.root_pt = pt; project_tree.reset(ptree_exp); } /** For undo purposes. */ public void resetRootTemplateThing(final TemplateThing tt, final HashMap<Thing,Boolean> ttree_exp) { this.root_tt = tt; template_tree.reset(ttree_exp); } /** For undo purposes. */ public void resetRootLayerThing(final LayerThing lt, final HashMap<Thing,Boolean> ltree_exp) { this.root_lt = lt; layer_tree.reset(ltree_exp); } public TemplateThing getRootTemplateThing() { return root_tt; } public LayerThing getRootLayerThing() { return root_lt; } public Bureaucrat saveTask(final String command) { return Bureaucrat.createAndStart(new Worker.Task("Saving") { public void exec() { if (command.equals("Save")) { save(); } else if (command.equals("Save as...")) { XMLOptions options = new XMLOptions(); options.overwriteXMLFile = false; options.export_images = false; options.include_coordinate_transform = true; options.patches_dir = null; // Will open a file dialog loader.saveAs(project, options); restartAutosaving(); // } else if (command.equals("Save as... without coordinate transforms")) { YesNoDialog yn = new YesNoDialog("WARNING", "You are about to save an XML file that lacks the information for the coordinate transforms of each image.\n" + "These transforms are referred to with the attribute 'ct_id' of each 't2_patch' entry in the XML document,\n" + "and the data for the transform is stored in an individual file under the folder 'trakem2.cts/'.\n" + " \n" + "It is advised to keep a complete XML file with all coordinate transforms included along with this new copy.\n" + "Please check NOW that you have such a complete XML copy.\n" + " \n" + "Proceed?"); if (!yn.yesPressed()) return; saveWithoutCoordinateTransforms(); // } else if (command.equals("Delete stale files...")) { setTaskName("Deleting stale files"); GenericDialog gd = new GenericDialog("Delete stale files"); gd.addMessage( "You are about to remove all files under the folder 'trakem2.cts/' which are not referred to from the\n" + "currently loaded project. If you have sibling XML files whose 't2_patch' entries (the images) refer,\n" + "via 'ct_id' attributes, to coordinate transforms in 'trakem2.cts/' that this current XML doesn't,\n" + "they may be LOST FOREVER. Unless you have a version of the XML file with the coordinate transforms\n" + "written in it, as can be obtained by using the 'Project - Save' command.\n" + " \n" + "The same is true for the .zip files that store alpha masks, under folder 'trakem2.masks/'\n" + "and which are referred to from the 'alpha_mask_id' attribute of 't2_patch' entries.\n" + " \n" + "Do you have such complete XML file? Check *NOW*.\n" + " \n" + "Proceed with deleting:" ); gd.addCheckbox("Delete stale coordinate transform files", true); gd.addCheckbox("Delete stale alpha mask files", true); gd.showDialog(); if (gd.wasCanceled()) return; project.getLoader().deleteStaleFiles(gd.getNextBoolean(), gd.getNextBoolean()); } } }, project); } /** The mode (aka algorithmic approach) used to generate mipmaps, which defaults to {@link Loader#DEFAULT_MIPMAPS_MODE}. */ public int getMipMapsMode() { return this.mipmaps_mode; } /** @see #getMipMapsMode() */ public void setMipMapsMode(int mode) { this.mipmaps_mode = mode; } }