fixed improper generic parameter use in Tree.duplicateAs > Map, necessary
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / Project.java
blob9a73f13a9716e6f8ac4bc0297af4a979296df508
1 /**
3 TrakEM2 plugin for ImageJ(C).
4 Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation (http://www.gnu.org/licenses/gpl.txt )
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 You may contact Albert Cardona at acardona at ini.phys.ethz.ch
20 Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
21 **/
23 package ini.trakem2;
26 import ij.IJ;
27 import ij.gui.GenericDialog;
28 import ij.io.DirectoryChooser;
29 import ini.trakem2.display.AreaList;
30 import ini.trakem2.display.AreaTree;
31 import ini.trakem2.display.Ball;
32 import ini.trakem2.display.Bucket;
33 import ini.trakem2.display.Connector;
34 import ini.trakem2.display.DLabel;
35 import ini.trakem2.display.Display;
36 import ini.trakem2.display.Displayable;
37 import ini.trakem2.display.Dissector;
38 import ini.trakem2.display.Layer;
39 import ini.trakem2.display.LayerSet;
40 import ini.trakem2.display.Patch;
41 import ini.trakem2.display.Pipe;
42 import ini.trakem2.display.Polyline;
43 import ini.trakem2.display.Profile;
44 import ini.trakem2.display.Stack;
45 import ini.trakem2.display.Treeline;
46 import ini.trakem2.display.YesNoDialog;
47 import ini.trakem2.display.ZDisplayable;
48 import ini.trakem2.persistence.DBLoader;
49 import ini.trakem2.persistence.DBObject;
50 import ini.trakem2.persistence.FSLoader;
51 import ini.trakem2.persistence.Loader;
52 import ini.trakem2.persistence.XMLOptions;
53 import ini.trakem2.plugin.TPlugIn;
54 import ini.trakem2.tree.DNDTree;
55 import ini.trakem2.tree.LayerThing;
56 import ini.trakem2.tree.LayerTree;
57 import ini.trakem2.tree.ProjectThing;
58 import ini.trakem2.tree.ProjectTree;
59 import ini.trakem2.tree.TemplateThing;
60 import ini.trakem2.tree.TemplateTree;
61 import ini.trakem2.tree.Thing;
62 import ini.trakem2.utils.Bureaucrat;
63 import ini.trakem2.utils.IJError;
64 import ini.trakem2.utils.ProjectToolbar;
65 import ini.trakem2.utils.Search;
66 import ini.trakem2.utils.Utils;
67 import ini.trakem2.utils.Worker;
69 import java.awt.Rectangle;
70 import java.io.BufferedReader;
71 import java.io.File;
72 import java.io.InputStreamReader;
73 import java.util.ArrayList;
74 import java.util.Arrays;
75 import java.util.Collections;
76 import java.util.Enumeration;
77 import java.util.HashMap;
78 import java.util.HashSet;
79 import java.util.Hashtable;
80 import java.util.Iterator;
81 import java.util.List;
82 import java.util.Map;
83 import java.util.Set;
84 import java.util.TreeMap;
85 import java.util.Vector;
86 import java.util.concurrent.ScheduledFuture;
87 import java.util.concurrent.TimeUnit;
88 import java.util.jar.JarEntry;
89 import java.util.jar.JarFile;
91 import javax.swing.JTree;
92 import javax.swing.UIManager;
93 import javax.swing.tree.DefaultMutableTreeNode;
94 import javax.swing.tree.TreePath;
96 /** The top-level class in control. */
97 public class Project extends DBObject {
99 static {
100 try {
101 //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
102 if (IJ.isLinux()) {
103 UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
104 if (null != IJ.getInstance()) javax.swing.SwingUtilities.updateComponentTreeUI(IJ.getInstance());
105 //if ("albert".equals(System.getProperty("user.name"))) UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
107 } catch (Exception e) {
108 Utils.log("Failed to set System Look and Feel");
113 static final private Vector<PlugInSource> PLUGIN_SOURCES = new Vector<PlugInSource>();
115 static private class PlugInSource implements Comparable<PlugInSource> {
116 String menu;
117 Class<?> c;
118 String title;
119 PlugInSource(String menu, Class<?> c, String title) {
120 this.menu = menu;
121 this.c = c;
122 this.title = title;
124 public int compareTo(PlugInSource ob) {
125 return ob.title.compareTo(this.title);
129 static {
130 // Search for plugins under fiji/plugins directory jar files
131 new Thread() { public void run() { try {
132 setPriority(Thread.NORM_PRIORITY);
133 setContextClassLoader(ij.IJ.getClassLoader());
134 final String plugins_dir = Utils.fixDir(ij.Menus.getPlugInsPath());
135 synchronized (PLUGIN_SOURCES) {
136 for (String name : new File(plugins_dir).list()) {
137 File f = new File(name);
138 if (f.isHidden() || !name.toLowerCase().endsWith(".jar")) continue;
139 JarFile jar = new JarFile(plugins_dir + name);
140 JarEntry entry = null;
141 for (Enumeration<JarEntry> en = jar.entries(); en.hasMoreElements(); ) {
142 JarEntry je = en.nextElement();
143 if (je.getName().endsWith(".trakem2")) {
144 entry = je;
145 break;
148 if (entry == null) continue;
149 // Parse:
150 BufferedReader br = new BufferedReader(new InputStreamReader(jar.getInputStream(entry)));
151 try {
152 while (true) {
153 String line = br.readLine();
154 if (null == line) break;
155 if (line.startsWith("#")) continue;
156 // tokenize:
157 // - from start to first comma is the menu
158 // - from first comma to last comma is the title
159 // - from last comma to end is the class
160 // The above allows for commas to be inside the title
161 int fc = line.indexOf(',');
162 if (-1 == fc) continue;
163 int lc = line.lastIndexOf(',');
164 if (-1 == lc) continue;
165 String menu = line.substring(0, fc).trim();
166 if (!menu.equals("Project Tree") && !menu.equals("Display")) continue;
167 String classname = line.substring(lc+1).trim();
168 try {
169 Class.forName(classname);
170 } catch (ClassNotFoundException cnfe) {
171 Utils.log2("TPlugIn class not found: " + classname);
172 continue;
174 int fq = line.indexOf('"', fc);
175 if (-1 == fq) continue;
176 int lq = line.lastIndexOf('"', lc);
177 if (-1 == lq) continue;
178 String title = line.substring(fq+1, lq).trim();
179 try {
180 PLUGIN_SOURCES.add(new PlugInSource(menu, Class.forName(classname), title));
181 Utils.log2("Found plugin for menu " + menu + " titled " + title + " for class " + classname);
182 } catch (ClassNotFoundException cnfe) {
183 Utils.log("Could not find TPlugIn class " + classname);
186 } finally {
187 br.close();
190 } catch (Throwable t) {
191 Utils.log("ERROR while parsing TrakEM2 plugins:");
192 IJError.print(t);
193 }}}.start();
196 /** Map of title keys vs TPlugin instances. */
197 private Map<PlugInSource,TPlugIn> plugins = null;
199 /** Create plugin instances for this project. */
200 synchronized private Map<PlugInSource,TPlugIn> createPlugins() {
201 final Map<PlugInSource,TPlugIn> m = Collections.synchronizedMap(new TreeMap<PlugInSource,TPlugIn>());
202 synchronized (PLUGIN_SOURCES) {
203 for (PlugInSource source : PLUGIN_SOURCES) {
204 try {
205 m.put(source, (TPlugIn)source.c.newInstance());
206 } catch (Exception e) {
207 Utils.log("ERROR initializing plugin!\nParsed tokens: [" + source.menu + "][" + source.title + "][" + source.c.getName() + "]");
208 IJError.print(e);
212 return m;
215 synchronized public TreeMap<String,TPlugIn> getPlugins(final String menu) {
216 final TreeMap<String,TPlugIn> m = new TreeMap<String,TPlugIn>();
217 if (null == plugins) plugins = createPlugins(); // to be created the first time it's asked for
218 for (Map.Entry<PlugInSource,TPlugIn> e : plugins.entrySet()) {
219 if (e.getKey().menu.equals(menu)) m.put(e.getKey().title, e.getValue());
221 return m;
224 /* // using virtual frame buffer instead, since the trees are needed
225 public static final boolean headless = isHeadless();
227 private static boolean isHeadless() {
228 return Boolean.parseBoolean(System.getProperty("java.awt.headless"));
232 /** Keep track of all open projects. */
233 static private ArrayList<Project> al_open_projects = new ArrayList<Project>();
235 private Loader loader;
237 private TemplateTree template_tree = null;
239 private ProjectTree project_tree = null;
241 /** The root Thing that holds the project. */
242 private ProjectThing root_pt;
244 /** The root LayerThing of the LayerTree. */
245 private LayerThing root_lt;
247 /** The root TemplateThing of the TemplateTree. */
248 private TemplateThing root_tt;
250 /** The root LayerSet that holds the layers. */
251 private LayerSet layer_set;
253 static private TemplateThing layer_template = null;
254 static private TemplateThing layer_set_template = null;
256 /** The table of unique TemplateThing types; the key is the type (String). */
257 private final Map<String,TemplateThing> ht_unique_tt = Collections.synchronizedMap(new HashMap<String,TemplateThing>());
259 private LayerTree layer_tree = null;
261 private String title = "Project";
263 private final HashMap<String,String> ht_props = new HashMap<String,String>();
265 private int mipmaps_mode = Loader.DEFAULT_MIPMAPS_MODE;
267 /** The constructor used by the static methods present in this class. */
268 private Project(Loader loader) {
269 super(loader);
270 ControlWindow.getInstance(); // init
271 this.loader = loader;
272 this.project = this; // for the superclass DBObject
273 loader.addToDatabase(this);
276 /** Constructor used by the Loader to find projects. These projects contain no loader. */
277 public Project(long id, String title) {
278 super(null, id);
279 ControlWindow.getInstance(); // init
280 this.title = title;
281 this.project = this;
284 private ScheduledFuture<?> autosaving = null;
286 private void restartAutosaving() {
287 // cancel current autosaving if it's running
288 if (null != autosaving) try {
289 autosaving.cancel(true);
290 } catch (Throwable t) { IJError.print(t); }
292 final int interval_in_minutes = getProperty("autosaving_interval", 0);
293 if (0 == interval_in_minutes) return;
294 // else, relaunch
295 this.autosaving = FSLoader.autosaver.scheduleWithFixedDelay(new Runnable() {
296 public void run() {
297 try {
298 if (loader.hasChanges()) {
299 Bureaucrat.createAndStart(new Worker.Task("auto-saving") {
300 @Override
301 public void exec() {
302 Project.this.save();
304 }, Project.this).join();
306 } catch (Throwable e) {
307 Utils.log("*** Autosaver failed:");
308 IJError.print(e);
311 }, interval_in_minutes * 60, interval_in_minutes * 60, TimeUnit.SECONDS);
314 static public Project getProject(final String title) {
315 for (final Project pr : al_open_projects) {
316 if (pr.title.equals(title)) return pr;
318 return null;
321 /** Return a copy of the list of all open projects. */
322 static public ArrayList<Project> getProjects() {
323 return new ArrayList<Project>(al_open_projects);
326 /** Create a new PostgreSQL-based TrakEM2 project. */
327 static public Project newDBProject() {
328 if (Utils.wrongImageJVersion()) return null;
329 // create
330 DBLoader loader = new DBLoader();
331 // check connection settings
332 if (!loader.isReady()) return null;
333 // check connection
334 if (!loader.isConnected()) {
335 Utils.showMessage("Can't talk to database.");
336 return null;
338 return createNewProject(loader, true);
341 /** Open a TrakEM2 project from the database. Queries the database for existing projects and if more than one, asks which one to open. */
342 static public Project openDBProject() {
343 if (Utils.wrongImageJVersion()) return null;
344 DBLoader loader = new DBLoader();
345 if (!loader.isReady()) return null;
346 // check connection
347 if (!loader.isConnected()) {
348 Utils.showMessage("Can't talk to database.");
349 loader.destroy();
350 return null;
352 // query the database for existing projects
353 Project[] projects = loader.getProjects();
354 if (null == projects) {
355 Utils.showMessage("Can't talk to database (null list of projects).");
356 loader.destroy();
357 return null;
359 Project project = null;
360 if (0 == projects.length) {
361 Utils.showMessage("No projects in this database.");
362 loader.destroy();
363 return null;
364 } else if (1 == projects.length) {
365 project = projects[0];
366 } else {
367 // ask to choose one
368 String[] titles = new String[projects.length];
369 for (int i=0; i<projects.length; i++) {
370 titles[i] = projects[i].title;
372 GenericDialog gd = new GenericDialog("Choose");
373 gd.addMessage("Choose project to open:");
374 gd.addChoice("project: ", titles, titles[titles.length -1]);
375 gd.showDialog();
376 if (gd.wasCanceled()) {
377 loader.destroy();
378 return null;
380 project = projects[gd.getNextChoiceIndex()];
382 // check if the selected project is open already
383 for (final Project p : al_open_projects) {
384 if (loader.isIdenticalProjectSource(p.loader) && p.id == project.id && p.title.equals(project.title)) {
385 Utils.showMessage("A project with title " + p.title + " and id " + p.id + " from the same database is already open.");
386 loader.destroy();
387 return null;
391 // now, open the selected project
393 // assign loader
394 project.loader = loader;
395 // grab the XML template
396 TemplateThing template_root = loader.getTemplateRoot(project);
397 if (null == template_root) {
398 Utils.showMessage("Failed to retrieve the template tree.");
399 project.destroy();
400 return null;
402 project.template_tree = new TemplateTree(project, template_root);
403 synchronized (project.ht_unique_tt) {
404 project.ht_unique_tt.clear();
405 project.ht_unique_tt.putAll(template_root.getUniqueTypes(new HashMap<String,TemplateThing>()));
407 // create the project Thing, to be root of the whole user Thing tree (and load all its objects)
408 HashMap<Long,Displayable> hs_d = new HashMap<Long,Displayable>(); // to collect all created displayables, and then reassign to the proper layers.
409 try {
410 // create a template for the project Thing
411 TemplateThing project_template = new TemplateThing("project");
412 project.ht_unique_tt.put("project", project_template);
413 project_template.addChild(template_root);
414 project.root_pt = loader.getRootProjectThing(project, template_root, project_template, hs_d);
415 // restore parent/child and attribute ownership and values (now that all Things exist)
416 project.root_pt.setup();
417 } catch (Exception e) {
418 Utils.showMessage("Failed to retrieve the Thing tree for the project.");
419 IJError.print(e);
420 project.destroy();
421 return null;
423 // create the user objects tree
424 project.project_tree = new ProjectTree(project, project.root_pt);
425 // restore the expanded state of each node
426 loader.restoreNodesExpandedState(project);
428 // create the layers templates
429 project.createLayerTemplates();
430 // 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.
431 LayerThing root_layer_thing = null;
432 try {
433 root_layer_thing = loader.getRootLayerThing(project, project.root_pt, Project.layer_set_template, Project.layer_template);
434 if (null == root_layer_thing) {
435 project.destroy();
436 Utils.showMessage("Could not retrieve the root layer thing.");
437 return null;
439 // set the child/parent relationships now that everything exists
440 root_layer_thing.setup();
441 project.layer_set = (LayerSet)root_layer_thing.getObject();
442 if (null == project.layer_set) {
443 project.destroy();
444 Utils.showMessage("Could not retrieve the root layer set.");
445 return null;
447 project.layer_set.setup(); // set the active layer to each ZDisplayable
449 // debug:
450 //Utils.log2("$$$ root_lt: " + root_layer_thing + " ob: " + root_layer_thing.getObject().getClass().getName() + "\n children: " + ((LayerSet)root_layer_thing.getObject()).getLayers().size());
452 project.layer_tree = new LayerTree(project, root_layer_thing);
453 project.root_lt = root_layer_thing;
454 } catch (Exception e) {
455 Utils.showMessage("Failed to retrieve the Layer tree for the project.");
456 IJError.print(e);
457 project.destroy();
458 return null;
461 // if all when well, register as open:
462 al_open_projects.add(project);
463 // create the project control window, containing the trees in a double JSplitPane
464 ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree);
465 // now open the displays that were stored for later, if any:
466 Display.openLater();
468 return project;
472 /** 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. */
473 static public Project newFSProject(String arg) {
474 return newFSProject(arg, null);
477 static public Project newFSProject(String arg, TemplateThing template_root) {
478 return newFSProject(arg, null, null);
481 /** Creates a new project to be based on .xml and image files, not a database.
482 * Images are left where they are, keeping the path to them.
483 * If the arg equals 'blank', then no template is asked for;
484 * if template_root is not null that is used; else, a template file is asked for.
486 * @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).
487 * @param template_root May be null, in which case a template DTD or XML file will be asked for, unless {@param arg} equals "blank".
488 * @param storage_folder If null, a dialog asks for it.
490 static public Project newFSProject(String arg, TemplateThing template_root, String storage_folder) {
491 return newFSProject(arg, template_root, storage_folder, true);
493 static public Project newFSProject(String arg, TemplateThing template_root, String storage_folder, boolean autocreate_one_layer) {
494 if (Utils.wrongImageJVersion()) return null;
495 FSLoader loader = null;
496 try {
497 String dir_project = storage_folder;
498 if (null == dir_project || !new File(dir_project).isDirectory()) {
499 DirectoryChooser dc = new DirectoryChooser("Select storage folder");
500 dir_project = dc.getDirectory();
501 if (null == dir_project) return null; // user cancelled dialog
502 if (!Loader.canReadAndWriteTo(dir_project)) {
503 Utils.showMessage("Can't read/write to the selected storage folder.\nPlease check folder permissions.");
504 return null;
506 if (IJ.isWindows()) dir_project = dir_project.replace('\\', '/');
508 loader = new FSLoader(dir_project);
510 Project project = createNewProject(loader, !("blank".equals(arg) || "amira".equals(arg)), template_root);
512 // help the helpless users:
513 if (autocreate_one_layer && null != project && ControlWindow.isGUIEnabled()) {
514 Utils.log2("Creating automatic Display.");
515 // add a default layer
516 Layer layer = new Layer(project, 0, 1, project.layer_set);
517 project.layer_set.add(layer);
518 project.layer_tree.addLayer(project.layer_set, layer);
519 layer.recreateBuckets();
520 Display.createDisplay(project, layer);
522 try {
523 Thread.sleep(200); // waiting cheaply for asynchronous swing calls
524 } catch (InterruptedException ie) {
525 ie.printStackTrace();
528 if ("amira".equals(arg) || "stack".equals(arg)) {
529 // forks into a task thread
530 loader.importStack(project.layer_set.getLayer(0), null, true);
533 project.restartAutosaving();
535 return project;
536 } catch (Exception e) {
537 IJError.print(e);
538 if (null != loader) loader.destroy();
540 return null;
543 static public Project openFSProject(final String path) {
544 return openFSProject(path, true);
547 /** Opens a project from an .xml file. If the path is null it'll be asked for.
548 * Only one project may be opened at a time.
550 @SuppressWarnings("unchecked")
551 synchronized static public Project openFSProject(final String path, final boolean open_displays) {
552 if (Utils.wrongImageJVersion()) return null;
553 final FSLoader loader = new FSLoader();
554 final Object[] data = loader.openFSProject(path, open_displays);
555 if (null == data) {
556 loader.destroy();
557 return null;
559 final TemplateThing root_tt = (TemplateThing)data[0];
560 final ProjectThing root_pt = (ProjectThing)data[1];
561 final LayerThing root_lt = (LayerThing)data[2];
562 final HashMap<ProjectThing,Boolean> ht_pt_expanded = (HashMap<ProjectThing,Boolean>)data[3];
564 final Project project = (Project)root_pt.getObject();
565 project.createLayerTemplates();
566 project.template_tree = new TemplateTree(project, root_tt);
567 project.root_tt = root_tt;
568 project.root_pt= root_pt;
569 project.project_tree = new ProjectTree(project, project.root_pt);
570 project.layer_tree = new LayerTree(project, root_lt);
571 project.root_lt = root_lt;
572 project.layer_set = (LayerSet)root_lt.getObject();
574 // if all when well, register as open:
575 al_open_projects.add(project);
576 // create the project control window, containing the trees in a double JSplitPane
577 ControlWindow.add(project, project.template_tree, project.project_tree, project.layer_tree);
579 // debug: print the entire root project tree
580 //project.root_pt.debug("");
582 // set ProjectThing nodes expanded state, now that the trees exist
583 try {
584 java.lang.reflect.Field f = JTree.class.getDeclaredField("expandedState");
585 f.setAccessible(true);
586 Hashtable<Object,Object> ht_exp = (Hashtable<Object,Object>) f.get(project.project_tree);
587 for (Map.Entry<ProjectThing,Boolean> entry : ht_pt_expanded.entrySet()) {
588 ProjectThing pt = entry.getKey();
589 Boolean expanded = entry.getValue();
590 //project.project_tree.expandPath(new TreePath(project.project_tree.findNode(pt, project.project_tree).getPath()));
591 // WARNING the above is wrong in that it will expand the whole thing, not just set the state of the node!!
592 // 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
593 // so, hackerous:
594 DefaultMutableTreeNode nd = DNDTree.findNode(pt, project.project_tree);
595 //if (null == nd) Utils.log2("null node for " + pt);
596 //else Utils.log2("path: " + new TreePath(nd.getPath()));
597 if (null == nd) {
598 Utils.log2("Can't find node for " + pt);
599 } else {
600 ht_exp.put(new TreePath(nd.getPath()), expanded);
603 project.project_tree.updateUILater(); // very important!!
604 } catch (Exception e) {
605 IJError.print(e);
607 // open any stored displays
608 if (open_displays) {
609 final Bureaucrat burro = Display.openLater();
610 if (null != burro) {
611 final Runnable ru = new Runnable() {
612 public void run() {
613 // wait until the Bureaucrat finishes
614 try { burro.join(); } catch (InterruptedException ie) {}
615 // restore to non-changes (crude, but works)
616 project.loader.setChanged(false);
617 Utils.log2("C set to false");
620 new Thread() {
621 public void run() {
622 setPriority(Thread.NORM_PRIORITY);
623 // avoiding "can't call invokeAndWait from the EventDispatch thread" error
624 try {
625 javax.swing.SwingUtilities.invokeAndWait(ru);
626 } catch (Exception e) {
627 Utils.log2("ERROR: " + e);
630 }.start();
631 // SO: WAIT TILL THE END OF TIME!
632 new Thread() { public void run() {
633 try {
634 Thread.sleep(4000); // ah, the pain in my veins. I can't take this shitty setup anymore.
635 javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() {
636 project.getLoader().setChanged(false);
637 Utils.log2("D set to false");
638 }});
639 project.getTemplateTree().updateUILater(); // repainting to fix gross errors in tree rendering
640 project.getProjectTree().updateUILater(); // idem
641 } catch (Exception ie) {}
642 }}.start();
643 } else {
644 // help the helpless users
645 Display.createDisplay(project, project.layer_set.getLayer(0));
649 project.restartAutosaving();
651 return project;
654 static private Project createNewProject(Loader loader, boolean ask_for_template) {
655 return createNewProject(loader, ask_for_template, null);
658 static private Project createNewProject(Loader loader, boolean ask_for_template, TemplateThing template_root) {
659 return createNewProject(loader, ask_for_template, template_root, false);
662 static private Project createNewProject(Loader loader, boolean ask_for_template, TemplateThing template_root, boolean clone_ids) {
663 Project project = new Project(loader);
664 // ask for an XML properties file that defines the Thing objects that can be created
665 // (the XML file will be parsed into a TemplateTree filled with TemplateThing objects)
666 //Utils.log2("ask_for_template: " + ask_for_template);
667 if (ask_for_template) template_root = project.loader.askForXMLTemplate(project);
668 if (null == template_root) {
669 template_root = new TemplateThing("anything");
670 } else if (clone_ids) {
671 // the given template_root belongs to another project from which we are cloning
672 template_root = template_root.clone(project, true);
673 } // else, use the given template_root as is.
674 // create tree
675 project.template_tree = new TemplateTree(project, template_root);
676 project.root_tt = template_root;
677 // collect unique TemplateThing instances
678 synchronized (project.ht_unique_tt) {
679 project.ht_unique_tt.clear();
680 project.ht_unique_tt.putAll(template_root.getUniqueTypes(new HashMap<String,TemplateThing>()));
682 // add all TemplateThing objects to the database, recursively
683 if (!clone_ids) template_root.addToDatabase(project);
684 // else already done when cloning the root_tt
686 // create a non-database bound template for the project Thing
687 TemplateThing project_template = new TemplateThing("project");
688 project.ht_unique_tt.put("project", project_template);
689 project_template.addChild(template_root);
690 // create the project Thing, to be root of the whole project thing tree
691 try {
692 project.root_pt= new ProjectThing(project_template, project, project);
693 } catch (Exception e) { IJError.print(e); }
694 // create the user objects tree
695 project.project_tree = new ProjectTree(project, project.root_pt);
696 // create the layer's tree
697 project.createLayerTemplates();
698 project.layer_set = new LayerSet(project, "Top Level", 0, 0, null, 2048, 2048); // initialized with default values, and null parent to signal 'root'
699 try {
700 project.root_lt = new LayerThing(Project.layer_set_template, project, project.layer_set);
701 project.layer_tree = new LayerTree(project, project.root_lt);
702 } catch (Exception e) {
703 project.remove();
704 IJError.print(e);
706 // create the project control window, containing the trees in a double JSplitPane
707 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.
708 // register
709 al_open_projects.add(project);
711 return project;
715 public void setTempLoader(Loader loader) {
716 if (null == this.loader) {
717 this.loader = loader;
718 } else {
719 Utils.log2("Project.setTempLoader: already have one.");
723 public final Loader getLoader() {
724 return loader;
727 /** Save the project regardless of what getLoader().hasChanges() reports. */
728 public String save() {
729 Thread.yield(); // let it repaint the log window
730 XMLOptions options = new XMLOptions();
731 options.overwriteXMLFile = true;
732 options.export_images = false;
733 options.patches_dir = null;
734 options.include_coordinate_transform = true;
735 String path = loader.save(this, options);
736 if (null != path) restartAutosaving();
737 return path;
740 /** This is not the saveAs used from the menus; this one is meant for programmatic access. */
741 public String saveAs(String xml_path, boolean overwrite) throws IllegalArgumentException {
742 if (null == xml_path) throw new IllegalArgumentException("xml_path cannot be null.");
743 XMLOptions options = new XMLOptions();
744 options.overwriteXMLFile = overwrite;
745 options.export_images = false;
746 options.patches_dir = null;
747 options.include_coordinate_transform = true;
748 String path = loader.saveAs(xml_path, options);
749 if (null != path) restartAutosaving();
750 return path;
753 /** Save an XML file that is stripped of coordinate transforms,
754 * and merely refers to them by the 'ct_id' attribute of each 't2_patch' element;
755 * this method will NOT overwrite the XML file but save into a new one,
756 * which is chosen from a file dialog. */
757 public String saveWithoutCoordinateTransforms() {
758 XMLOptions options = new XMLOptions();
759 options.overwriteXMLFile = false;
760 options.export_images = false;
761 options.include_coordinate_transform = false;
762 options.patches_dir = null;
763 return loader.saveAs(this, options);
766 public boolean destroy() {
767 if (null == loader) {
768 return true;
770 if (loader.hasChanges() && !getBooleanProperty("no_shutdown_hook")) { // DBLoader always returns false
771 if (ControlWindow.isGUIEnabled()) {
772 final YesNoDialog yn = ControlWindow.makeYesNoDialog("TrakEM2", "There are unsaved changes in project " + title + ". Save them?");
773 if (yn.yesPressed()) {
774 save();
776 } else {
777 Utils.log2("WARNING: closing project '" + title + "' with unsaved changes.");
780 try {
781 if (null != autosaving) autosaving.cancel(true);
782 } catch (Throwable t) {}
783 al_open_projects.remove(this);
784 // flush all memory
785 if (null != loader) { // the last project is destroyed twice for some reason, if several are open. This is a PATCH
786 loader.destroy(); // and disconnect
787 loader = null;
789 if (null != layer_set) layer_set.destroy();
790 ControlWindow.remove(this); // AFTER loader.destroy() call.
791 if (null != template_tree) template_tree.destroy();
792 if (null != project_tree) project_tree.destroy();
793 if (null != layer_tree) layer_tree.destroy();
794 Polyline.flushTraceCache(this);
795 this.template_tree = null; // flag to mean: we're closing
796 // close all open Displays
797 Display.close(this);
798 Search.removeTabs(this);
799 synchronized (ptcache) { ptcache.clear(); }
800 return true;
803 public boolean isBeingDestroyed() {
804 return null == template_tree;
807 /** Remove the project from the database and release memory. */
808 public void remove() {
809 removeFromDatabase();
810 destroy();
813 /** Remove the project from the database and release memory. */
814 public boolean remove(boolean check) {
815 if (!Utils.check("Delete the project " + toString() + " from the database?")) return false;
816 removeFromDatabase();
817 destroy();
818 return true;
821 public void setTitle(String title) {
822 if (null == title) return;
823 this.title = title;
824 ControlWindow.updateTitle(this);
825 loader.updateInDatabase(this, "title");
828 public String toString() {
829 if (null == title || title.equals("Project")) {
830 try {
831 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.
832 } catch (Exception e) { Utils.log2("Swing again."); }
834 return title;
837 public String getTitle() {
838 return title;
841 public TemplateTree getTemplateTree() {
842 return template_tree;
845 public LayerTree getLayerTree() {
846 return layer_tree;
849 public ProjectTree getProjectTree() {
850 return project_tree;
853 /** Make an object of the type the TemplateThing can hold. */
854 public Object makeObject(final TemplateThing tt) {
855 final String type = tt.getType();
856 if (type.equals("profile")) {
857 ProjectToolbar.setTool(ProjectToolbar.PENCIL); // this should go elsewhere, in display issues.
858 return new Profile(this, "profile", 0, 0);
859 } else if (type.equals("pipe")) {
860 ProjectToolbar.setTool(ProjectToolbar.PEN);
861 return new Pipe(this, "pipe", 0, 0);
862 } else if (type.equals("polyline")) {
863 ProjectToolbar.setTool(ProjectToolbar.PEN);
864 return new Polyline(this, "polyline");
865 } else if (type.equals("area_list")) {
866 ProjectToolbar.setTool(ProjectToolbar.BRUSH);
867 return new AreaList(this, "area_list", 0, 0);
868 } else if (type.equals("treeline")) {
869 ProjectToolbar.setTool(ProjectToolbar.PEN);
870 return new Treeline(this, "treeline");
871 } else if (type.equals("areatree")) {
872 ProjectToolbar.setTool(ProjectToolbar.PEN);
873 return new AreaTree(this, "areatree");
874 } else if (type.equals("ball")) {
875 ProjectToolbar.setTool(ProjectToolbar.PEN);
876 return new Ball(this, "ball", 0, 0);
877 } else if (type.equals("connector")) {
878 ProjectToolbar.setTool(ProjectToolbar.PEN);
879 return new Connector(this, "connector");
880 } else if (type.equals("dissector")) {
881 ProjectToolbar.setTool(ProjectToolbar.PEN);
882 return new Dissector(this, "dissector", 0, 0);
883 } else if (type.equals("label")) {
884 return new DLabel(this, " ", 0, 0); // never used so far
885 } else {
886 // just the name, for the abstract ones
887 return type;
891 /** Returns true if the type is 'patch', 'layer', 'layer_set', 'profile', 'profile_list' 'pipe'. */
892 static public boolean isBasicType(final String type) {
893 return isProjectType(type)
894 || isLayerSetType(type)
895 || isLayerType(type)
899 static public boolean isProjectType(String type) {
900 type = type.toLowerCase();
901 return type.equals("profile_list");
904 static public boolean isLayerSetType(String type) {
905 type = type.toLowerCase().replace(' ', '_');
906 return type.equals("area_list")
907 || type.equals("pipe")
908 || type.equals("ball")
909 || type.equals("polyline")
910 || type.equals("dissector")
911 || type.equals("stack")
912 || type.equals("treeline")
913 || type.equals("areatree")
914 || type.equals("connector")
918 static public boolean isLayerType(String type) {
919 type = type.toLowerCase().replace(' ', '_');
920 return type.equals("patch")
921 || type.equals("profile")
922 || type.equals("layer")
923 || type.equals("layer_set") // for XML
924 || type.equals("label")
928 /** Remove the ProjectThing that contains the given object, which will remove the object itself as well. */
929 public boolean removeProjectThing(Object object, boolean check) {
930 return removeProjectThing(object, check, false, 0);
933 /** Remove the ProjectThing that contains the given object, which will remove the object itself as well. */
934 public boolean removeProjectThing(Object object, boolean check, boolean remove_empty_parents, int levels) {
935 if (levels < 0) {
936 Utils.log2("Project.removeProjectThing: levels must be zero or above.");
937 return false;
939 // find the Thing
940 DefaultMutableTreeNode root = (DefaultMutableTreeNode)project_tree.getModel().getRoot();
941 Enumeration<?> e = root.depthFirstEnumeration();
942 DefaultMutableTreeNode node = null;
943 while (e.hasMoreElements()) {
944 node = (DefaultMutableTreeNode)e.nextElement();
945 Object ob = node.getUserObject();
946 if (ob instanceof ProjectThing && ((ProjectThing)ob).getObject() == object) {
947 if (check && !Utils.check("Remove " + object.toString() + "?")) return false;
948 // remove the ProjectThing, its object and the node that holds it.
949 project_tree.remove(node, false, remove_empty_parents, levels);
950 return true;
951 } // 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.
953 // not found:
954 return false;
957 /** Find the node in the layer tree with a Thing that contains the given object, and set it selected/highlighted, deselecting everything else first. */
958 public void select(final Layer layer) {
959 layer_tree.selectNode(layer);
961 /** Find the node in any tree with a Thing that contains the given Displayable, and set it selected/highlighted, deselecting everything else first. */
962 public void select(final Displayable d) {
963 if (d.getClass() == LayerSet.class) select(d, layer_tree);
964 else {
965 ProjectThing pt = findProjectThing(d); // from cache: one linear search less
966 if (null != pt) DNDTree.selectNode(pt, project_tree);
970 private final void select(final Object ob, final DNDTree tree) {
971 // Find the Thing that contains the object
972 final Thing root_thing = (Thing)((DefaultMutableTreeNode)tree.getModel().getRoot()).getUserObject();
973 final Thing child_thing = root_thing.findChild(ob);
974 // find the node that contains the Thing, and select it
975 DNDTree.selectNode(child_thing, tree);
978 /** Find the ProjectThing instance with the given id. */
979 public ProjectThing find(final long id) {
980 // can't be the Project itself
981 return root_pt.findChild(id);
984 public DBObject findById(final long id) {
985 if (this.id == id) return this;
986 DBObject dbo = layer_set.findById(id);
987 if (null != dbo) return dbo;
988 dbo = root_pt.findChild(id); // could call findObject(id), but all objects must exist in layer sets anyway.
989 if (null != dbo) return dbo;
990 return (DBObject)root_tt.findChild(id);
993 /** Find a LayerThing that contains the given object. */
994 public LayerThing findLayerThing(final Object ob) {
995 final Object lob = root_lt.findChild(ob);
996 return null != lob ? (LayerThing)lob : null;
999 private final Map<Object,ProjectThing> ptcache = new HashMap<Object, ProjectThing>();
1001 /** Find a ProjectThing that contains the given object. */
1002 public ProjectThing findProjectThing(final Object ob) {
1003 ProjectThing pt;
1004 synchronized (ptcache) { pt = ptcache.get(ob); }
1005 if (null == pt) {
1006 pt = (ProjectThing) root_pt.findChild(ob);
1007 if (null != ob) synchronized (ptcache) { ptcache.put(ob, pt); }
1009 return pt;
1012 public void decache(final Object ob) {
1013 synchronized (ptcache) {
1014 ptcache.remove(ob);
1018 public ProjectThing getRootProjectThing() {
1019 return root_pt;
1022 public LayerSet getRootLayerSet() {
1023 return layer_set;
1026 /** Returns the title of the enclosing abstract node in the ProjectTree.*/
1027 public String getParentTitle(final Displayable d) {
1028 try {
1029 ProjectThing thing = findProjectThing(d);
1030 ProjectThing parent = (ProjectThing)thing.getParent();
1031 if (d instanceof Profile) {
1032 parent = (ProjectThing)parent.getParent(); // skip the profile_list
1034 if (null == parent) Utils.log2("null parent for " + d);
1035 if (null != parent && null == parent.getObject()) {
1036 Utils.log2("null ob for parent " + parent + " of " + d);
1038 return parent.getObject().toString(); // the abstract thing should be enclosing a String object
1039 } catch (Exception e) { IJError.print(e); return null; }
1042 public String getMeaningfulTitle2(final Displayable d) {
1043 final ProjectThing thing = findProjectThing(d);
1044 if (null == thing) return d.getTitle(); // happens if there is no associated node
1046 if (!thing.getType().equals(d.getTitle())) {
1047 return new StringBuilder(!thing.getType().equals(d.getTitle()) ? d.getTitle() + " [" : "[").append(thing.getType()).append(']').toString();
1050 // Else, search upstream for a ProjectThing whose name differs from its type
1051 Thing parent = (ProjectThing)thing.getParent();
1052 while (null != parent) {
1053 String type = parent.getType();
1054 Object ob = parent.getObject();
1055 if (ob.getClass() == Project.class) break;
1056 if (!ob.equals(type)) {
1057 return ob.toString() + " [" + thing.getType() + "]";
1059 parent = parent.getParent();
1061 if (d.getTitle().equals(thing.getType())) return "[" + thing.getType() + "]";
1062 return d.getTitle() + " [" + thing.getType() + "]";
1065 /** 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. */
1066 public String getMeaningfulTitle(final Displayable d) {
1067 ProjectThing thing = findProjectThing(d);
1068 if (null == thing) return d.getTitle(); // happens if there is no associated node
1069 String title = new StringBuilder(!thing.getType().equals(d.getTitle()) ? d.getTitle() + " [" : "[").append(thing.getType()).append(' ').append('#').append(d.getId()).append(']').toString();
1071 if (!thing.getType().equals(d.getTitle())) {
1072 return title;
1075 ProjectThing parent = (ProjectThing)thing.getParent();
1076 StringBuilder sb = new StringBuilder(title);
1077 while (null != parent) {
1078 Object ob = parent.getObject();
1079 if (ob.getClass() == Project.class) break;
1080 String type = parent.getType();
1081 if (!ob.equals(type)) { // meaning, something else was typed in as a title
1082 sb.insert(0, new StringBuilder(ob.toString()).append(' ').append('[').append(type).append(']').append('/').toString());
1083 //title = ob.toString() + " [" + type + "]/" + title;
1084 break;
1086 sb.insert(0, '/');
1087 sb.insert(0, type);
1088 //title = type + "/" + title;
1089 parent = (ProjectThing)parent.getParent();
1091 //return title;
1092 return sb.toString();
1095 /** Returns the first upstream user-defined name and type, and the id of the displayable tagged at the end.
1096 * If no user-defined name is found, then the type is prepended to the id.
1098 public String getShortMeaningfulTitle(final Displayable d) {
1099 ProjectThing thing = findProjectThing(d);
1100 if (null == thing) return d.getTitle(); // happens if there is no associated node
1101 return getShortMeaningfulTitle(thing, d);
1103 public String getShortMeaningfulTitle(final ProjectThing thing, final Displayable d) {
1104 if (thing.getObject() != d) {
1105 return thing.toString();
1107 ProjectThing parent = (ProjectThing)thing.getParent();
1108 String title = "#" + d.getId();
1109 while (null != parent) {
1110 Object ob = parent.getObject();
1111 String type = parent.getType();
1112 if (!ob.equals(type)) { // meaning, something else was typed in as a title
1113 title = ob.toString() + " [" + type + "] " + title;
1114 break;
1116 parent = (ProjectThing)parent.getParent();
1118 // if nothing found, prepend the type
1119 if ('#' == title.charAt(0)) title = Project.getName(d.getClass()) + " " + title;
1120 return title;
1123 static public String getType(final Class<?> c) {
1124 if (AreaList.class == c) return "area_list";
1125 if (DLabel.class == c) return "label";
1126 String name = c.getName().toLowerCase();
1127 int i = name.lastIndexOf('.');
1128 if (-1 != i) name = name.substring(i+1);
1129 return name;
1132 /** Returns the proper TemplateThing for the given type, complete with children and attributes if any. */
1133 public TemplateThing getTemplateThing(String type) {
1134 return ht_unique_tt.get(type);
1137 /** Returns a list of existing unique types in the template tree
1138 * (thus the 'project' type is not included, nor the label).
1139 * The basic types are guaranteed to be present even if there are no instances in the template tree.
1140 * As a side effect, this method populates the HashMap of unique TemplateThing types. */
1141 public String[] getUniqueTypes() {
1142 synchronized (ht_unique_tt) {
1143 // ensure the basic types (pipe, ball, profile, profile_list) are present
1144 if (!ht_unique_tt.containsKey("profile")) ht_unique_tt.put("profile", new TemplateThing("profile"));
1145 if (!ht_unique_tt.containsKey("profile_list")) {
1146 TemplateThing tpl = new TemplateThing("profile_list");
1147 tpl.addChild((TemplateThing) ht_unique_tt.get("profile"));
1148 ht_unique_tt.put("profile_list", tpl);
1150 if (!ht_unique_tt.containsKey("pipe")) ht_unique_tt.put("pipe", new TemplateThing("pipe"));
1151 if (!ht_unique_tt.containsKey("polyline")) ht_unique_tt.put("polyline", new TemplateThing("polyline"));
1152 if (!ht_unique_tt.containsKey("treeline")) ht_unique_tt.put("treeline", new TemplateThing("treeline"));
1153 if (!ht_unique_tt.containsKey("areatree")) ht_unique_tt.put("areatree", new TemplateThing("areatree"));
1154 if (!ht_unique_tt.containsKey("connector")) ht_unique_tt.put("connector", new TemplateThing("connector"));
1155 if (!ht_unique_tt.containsKey("ball")) ht_unique_tt.put("ball", new TemplateThing("ball"));
1156 if (!ht_unique_tt.containsKey("area_list")) ht_unique_tt.put("area_list", new TemplateThing("area_list"));
1157 if (!ht_unique_tt.containsKey("dissector")) ht_unique_tt.put("dissector", new TemplateThing("dissector"));
1158 // 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.
1160 TemplateThing project_tt = ht_unique_tt.remove("project");
1161 /* // debug
1162 for (Iterator it = ht_unique_tt.keySet().iterator(); it.hasNext(); ) {
1163 Utils.log2("class: " + it.next().getClass().getName());
1164 } */
1165 final String[] ut = new String[ht_unique_tt.size()];
1166 ht_unique_tt.keySet().toArray(ut);
1167 ht_unique_tt.put("project", project_tt);
1168 Arrays.sort(ut);
1169 return ut;
1173 /** Remove a unique type from the HashMap. Basic types can't be removed. */
1174 public boolean removeUniqueType(String type) {
1175 if (null == type || isBasicType(type)) return false;
1176 synchronized (ht_unique_tt) {
1177 return null != ht_unique_tt.remove(type);
1181 public boolean typeExists(String type) {
1182 return ht_unique_tt.containsKey(type);
1185 /** Returns false if the type exists already. */
1186 public boolean addUniqueType(TemplateThing tt) {
1187 synchronized (ht_unique_tt) {
1188 if (ht_unique_tt.containsKey(tt.getType())) return false;
1189 ht_unique_tt.put(tt.getType(), tt);
1191 return true;
1194 public boolean updateTypeName(String old_type, String new_type) {
1195 synchronized (ht_unique_tt) {
1196 if (ht_unique_tt.containsKey(new_type)) {
1197 Utils.showMessage("Can't rename type '" + old_type + "' : a type named '"+new_type+"' already exists!");
1198 return false;
1200 ht_unique_tt.put(new_type, ht_unique_tt.remove(old_type));
1201 return true;
1205 private void createLayerTemplates() {
1206 if (null == layer_template) {
1207 layer_template = new TemplateThing("layer");
1208 layer_set_template = new TemplateThing("layer_set");
1209 layer_set_template.addChild(layer_template);
1210 layer_template.addChild(layer_set_template); // adding a new instance to keep parent/child relationships clean
1211 // No need, there won't ever be a loop so far WARNING may change in the future.
1215 @Override
1216 public void exportXML(final StringBuilder sb, final String indent, final XMLOptions options) {
1217 Utils.logAll("ERROR: cannot call Project.exportXML(StringBuilder, String, ExportOptions) !!");
1218 throw new UnsupportedOperationException("Cannot call Project.exportXML(StringBuilder, String, Object)");
1221 /** 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. */
1222 public void exportXML(final java.io.Writer writer, final String indent, final XMLOptions options) throws Exception {
1223 Utils.showProgress(0);
1224 // 1 - opening tag
1225 writer.write(indent);
1226 writer.write("<trakem2>\n");
1227 final String in = indent + "\t";
1228 // 2,3 - export the project itself
1229 exportXML2(writer, in, options);
1230 // 4 - export LayerSet hierarchy of Layer, LayerSet and Displayable objects
1231 layer_set.exportXML(writer, in, options);
1232 // 5 - export Display objects
1233 Display.exportXML(this, writer, in, options);
1234 // 6 - closing tag
1235 writer.write("</trakem2>\n");
1238 // A separate method to ensure that sb_body instance is garbage collected.
1239 private final void exportXML2(final java.io.Writer writer, final String in, final XMLOptions options) throws Exception {
1240 final StringBuilder sb_body = new StringBuilder();
1241 // 2 - the project itself
1242 sb_body.append(in).append("<project \n")
1243 .append(in).append("\tid=\"").append(id).append("\"\n")
1244 .append(in).append("\ttitle=\"").append(title).append("\"\n");
1245 loader.insertXMLOptions(sb_body, in + "\t");
1246 // Write properties, with the additional property of the image_resizing_mode
1247 final HashMap<String,String> props = new HashMap<String, String>(ht_props);
1248 props.put("image_resizing_mode", Loader.getMipMapModeName(mipmaps_mode));
1249 for (final Map.Entry<String, String> e : props.entrySet()) {
1250 sb_body.append(in).append('\t').append(e.getKey()).append("=\"").append(e.getValue()).append("\"\n");
1252 sb_body.append(in).append(">\n");
1253 // 3 - export ProjectTree abstract hierarchy (skip the root since it wraps the project itself)
1254 project_tree.getExpandedStates(options.expanded_states);
1255 if (null != root_pt.getChildren()) {
1256 final String in2 = in + "\t";
1257 for (final ProjectThing pt : root_pt.getChildren()) {
1258 pt.exportXML(sb_body, in2, options);
1261 sb_body.append(in).append("</project>\n");
1262 writer.write(sb_body.toString());
1265 /** Export a complete DTD listing to export the project as XML. */
1266 public void exportDTD(final StringBuilder sb_header, final HashSet<String> hs, final String indent) {
1267 // 1 - TrakEM2 tag that encloses all hierarchies
1268 sb_header.append(indent).append("<!ELEMENT ").append("trakem2 (project,t2_layer_set,t2_display)>\n");
1269 // 2 - export user-defined templates
1270 //TemplateThing root_tt = (TemplateThing)((DefaultMutableTreeNode)((DefaultTreeModel)template_tree.getModel()).getRoot()).getUserObject();
1271 sb_header.append(indent).append("<!ELEMENT ").append("project (").append(root_tt.getType()).append(")>\n");
1272 sb_header.append(indent).append("<!ATTLIST project id NMTOKEN #REQUIRED>\n");
1273 sb_header.append(indent).append("<!ATTLIST project unuid NMTOKEN #REQUIRED>\n");
1274 sb_header.append(indent).append("<!ATTLIST project title NMTOKEN #REQUIRED>\n");
1275 sb_header.append(indent).append("<!ATTLIST project preprocessor NMTOKEN #REQUIRED>\n");
1276 sb_header.append(indent).append("<!ATTLIST project mipmaps_folder NMTOKEN #REQUIRED>\n");
1277 sb_header.append(indent).append("<!ATTLIST project storage_folder NMTOKEN #REQUIRED>\n");
1278 for (String key : ht_props.keySet()) {
1279 sb_header.append(indent).append("<!ATTLIST project ").append(key).append(" NMTOKEN #REQUIRED>\n");
1281 root_tt.exportDTD(sb_header, hs, indent);
1282 // 3 - export all project objects DTD in the Top Level LayerSet
1283 Layer.exportDTD(sb_header, hs, indent);
1284 LayerSet.exportDTD(sb_header, hs, indent);
1285 Ball.exportDTD(sb_header, hs, indent);
1286 DLabel.exportDTD(sb_header, hs, indent);
1287 Patch.exportDTD(sb_header, hs, indent);
1288 Pipe.exportDTD(sb_header, hs, indent);
1289 Polyline.exportDTD(sb_header, hs, indent);
1290 Profile.exportDTD(sb_header, hs, indent);
1291 AreaList.exportDTD(sb_header, hs, indent);
1292 Dissector.exportDTD(sb_header, hs, indent);
1293 Stack.exportDTD( sb_header, hs, indent );
1294 Treeline.exportDTD(sb_header, hs, indent);
1295 AreaTree.exportDTD(sb_header, hs, indent);
1296 Connector.exportDTD(sb_header, hs, indent);
1297 Displayable.exportDTD(sb_header, hs, indent); // the subtypes of all Displayable types
1298 // 4 - export Display
1299 Display.exportDTD(sb_header, hs, indent);
1300 // all the above could be done with reflection, automatically detecting the presence of an exportDTD method.
1301 // CoordinateTransforms
1302 mpicbg.trakem2.transform.DTD.append( sb_header, hs, indent );
1305 /** Returns the String to be used as Document Type of the XML file, generated from the name of the root template thing.*/
1306 public String getDocType() {
1307 //TemplateThing root_tt = (TemplateThing)((DefaultMutableTreeNode)((DefaultTreeModel)template_tree.getModel()).getRoot()).getUserObject();
1308 return "trakem2_" + root_tt.getType();
1311 /** Returns a user-understandable name for the given class. */
1312 static public String getName(final Class<?> c) {
1313 String name = c.getName();
1314 name = name.substring(name.lastIndexOf('.') + 1);
1315 if (name.equals("DLabel")) return "Label";
1316 else if (name.equals("Patch")) return "Image";
1317 //else if (name.equals("Pipe")) return "Tube";
1318 //else if (name.equals("Ball")) return "Sphere group"; // TODO revise consistency with XML templates and so on
1319 else return name;
1322 public String getInfo() {
1323 StringBuilder sb = new StringBuilder("Project id: ");
1324 sb.append(this.id).append("\nProject name: ").append(this.title)
1325 .append("\nTrees:\n")
1326 .append(project_tree.getInfo()).append("\n")
1327 .append(layer_tree.getInfo())
1329 return sb.toString();
1332 static public Project findProject(Loader loader) {
1333 for (final Project pro : al_open_projects) {
1334 if (pro.getLoader() == loader) return pro;
1336 return null;
1339 private boolean input_disabled = false;
1341 /** Tells the displays concerning this Project to accept/reject input. */
1342 public void setReceivesInput(boolean b) {
1343 this.input_disabled = !b;
1344 Display.setReceivesInput(this, b);
1347 public boolean isInputEnabled() {
1348 return !input_disabled;
1351 /** Create a new subproject for the given layer range and ROI.
1352 * 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. */
1353 public Project createSubproject(final Rectangle roi, final Layer first, final Layer last, final boolean ignore_hidden_patches) {
1354 try {
1355 // The order matters.
1356 final Project pr = new Project(new FSLoader(this.getLoader().getStorageFolder()));
1357 pr.id = this.id;
1358 // copy properties
1359 pr.title = this.title;
1360 pr.ht_props.putAll(this.ht_props);
1361 // copy template
1362 pr.root_tt = this.root_tt.clone(pr, true);
1363 pr.template_tree = new TemplateTree(pr, pr.root_tt);
1364 synchronized (pr.ht_unique_tt) {
1365 pr.ht_unique_tt.clear();
1366 pr.ht_unique_tt.putAll(root_tt.getUniqueTypes(new HashMap<String,TemplateThing>()));
1368 TemplateThing project_template = new TemplateThing("project");
1369 project_template.addChild(pr.root_tt);
1370 pr.ht_unique_tt.put("project", project_template);
1371 // create the layers templates
1372 pr.createLayerTemplates();
1373 // copy LayerSet and all involved Displayable objects
1374 // (A two-step process to provide the layer_set pointer and all Layer pointers to the ZDisplayable to copy and crop.)
1375 pr.layer_set = (LayerSet)this.layer_set.clone(pr, first, last, roi, false, true, ignore_hidden_patches);
1376 LayerSet.cloneInto(this.layer_set, first, last, pr, pr.layer_set, roi, true);
1377 // create layer tree
1378 pr.root_lt = new LayerThing(Project.layer_set_template, pr, pr.layer_set);
1379 pr.layer_tree = new LayerTree(pr, pr.root_lt);
1380 // add layer nodes to the layer tree (solving chicken-and-egg problem)
1381 pr.layer_set.updateLayerTree();
1382 // copy project tree
1383 pr.root_pt = this.root_pt.subclone(pr);
1384 pr.project_tree = new ProjectTree(pr, pr.root_pt);
1385 // not copying node expanded state.
1386 // register
1387 al_open_projects.add(pr);
1388 // add to gui:
1389 ControlWindow.add(pr, pr.template_tree, pr.project_tree, pr.layer_tree);
1391 // Above, the id of each object is preserved from this project into the subproject.
1393 // The abstract structure should be copied in full regardless, without the basic objects
1394 // included if they intersect the roi.
1396 // Regenerate mipmaps (blocks GUI from interaction other than navigation)
1397 pr.loader.regenerateMipMaps(pr.layer_set.getDisplayables(Patch.class));
1399 pr.restartAutosaving();
1401 return pr;
1403 } catch (Exception e) { e.printStackTrace(); }
1404 return null;
1407 public void parseXMLOptions(final HashMap<String,String> ht_attributes) {
1408 ((FSLoader)this.project.getLoader()).parseXMLOptions(ht_attributes);
1410 String mipmapsMode = ht_attributes.remove("image_resizing_mode");
1411 this.mipmaps_mode = null == mipmapsMode ? Loader.DEFAULT_MIPMAPS_MODE : Loader.getMipMapModeIndex(mipmapsMode);
1413 // all keys that remain are properties
1414 ht_props.putAll(ht_attributes);
1415 for (Map.Entry<String,String> prop : ht_attributes.entrySet()) {
1416 Utils.log2("parsed: " + prop.getKey() + "=" + prop.getValue());
1419 public HashMap<String,String> getPropertiesCopy() {
1420 return new HashMap<String,String>(ht_props);
1422 /** Returns null if not defined. */
1423 public String getProperty(final String key) {
1424 return ht_props.get(key);
1426 /** Returns the default value if not defined, or if not a number or not parsable as a number. */
1427 public float getProperty(final String key, final float default_value) {
1428 try {
1429 final String s = ht_props.get(key);
1430 if (null == s) return default_value;
1431 final float num = Float.parseFloat(s);
1432 if (Float.isNaN(num)) return default_value;
1433 return num;
1434 } catch (NumberFormatException nfe) {
1435 IJError.print(nfe);
1437 return default_value;
1440 public int getProperty(final String key, final int default_value) {
1441 try {
1442 final String s = ht_props.get(key);
1443 if (null == s) return default_value;
1444 return Integer.parseInt(s);
1445 } catch (NumberFormatException nfe) {
1446 IJError.print(nfe);
1448 return default_value;
1451 public boolean getBooleanProperty(final String key) {
1452 return "true".equals(ht_props.get(key));
1454 public void setProperty(final String key, final String value) {
1455 if (null == value) ht_props.remove(key);
1456 else ht_props.put(key, value);
1458 private final boolean addBox(final GenericDialog gd, final Class<?> c) {
1459 final String name = Project.getName(c);
1460 final boolean link = "true".equals(ht_props.get(name.toLowerCase() + "_nolinks"));
1461 gd.addCheckbox(name, link);
1462 return link;
1464 private final void setLinkProp(final boolean before, final boolean after, final Class<?> c) {
1465 if (before) {
1466 if (!after) ht_props.remove(Project.getName(c).toLowerCase()+"_nolinks");
1467 } else if (after) {
1468 ht_props.put(Project.getName(c).toLowerCase()+"_nolinks", "true");
1470 // setting to false would have no meaning, so the link prop is removed
1472 /** Returns true if there were any changes. */
1473 private final boolean adjustProp(final String prop, final boolean before, final boolean after) {
1474 if (before) {
1475 if (!after) ht_props.remove(prop);
1476 } else if (after) {
1477 ht_props.put(prop, "true");
1479 return before != after;
1481 public void adjustProperties() {
1482 // should be more generic, but for now it'll do
1483 GenericDialog gd = new GenericDialog("Properties");
1484 gd.addMessage("Ignore image linking for:");
1485 boolean link_labels = addBox(gd, DLabel.class);
1486 boolean nolink_segmentations = "true".equals(ht_props.get("segmentations_nolinks"));
1487 gd.addCheckbox("Segmentations", nolink_segmentations);
1488 gd.addMessage("Currently linked objects will remain so\nunless explicitly unlinked.");
1489 boolean dissector_zoom = "true".equals(ht_props.get("dissector_zoom"));
1490 gd.addCheckbox("Zoom-invariant markers for Dissector", dissector_zoom);
1491 gd.addChoice("Image_resizing_mode: ", Loader.MIPMAP_MODES.values().toArray(new String[Loader.MIPMAP_MODES.size()]), Loader.getMipMapModeName(mipmaps_mode));
1492 gd.addChoice("mipmaps format:", FSLoader.MIPMAP_FORMATS, FSLoader.MIPMAP_FORMATS[loader.getMipMapFormat()]);
1493 boolean layer_mipmaps = "true".equals(ht_props.get("layer_mipmaps"));
1494 gd.addCheckbox("Layer_mipmaps", layer_mipmaps);
1495 boolean keep_mipmaps = "true".equals(ht_props.get("keep_mipmaps"));
1496 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.
1497 int bucket_side = (int)getProperty("bucket_side", Bucket.MIN_BUCKET_SIZE);
1498 gd.addNumericField("Bucket side length: ", bucket_side, 0, 6, "pixels");
1499 boolean no_shutdown_hook = "true".equals(ht_props.get("no_shutdown_hook"));
1500 gd.addCheckbox("No_shutdown_hook to save the project", no_shutdown_hook);
1501 int n_undo_steps = getProperty("n_undo_steps", 32);
1502 gd.addSlider("Undo steps", 32, 200, n_undo_steps);
1503 boolean flood_fill_to_image_edge = "true".equals(ht_props.get("flood_fill_to_image_edge"));
1504 gd.addCheckbox("AreaList_flood_fill_to_image_edges", flood_fill_to_image_edge);
1505 int look_ahead_cache = (int)getProperty("look_ahead_cache", 0);
1506 gd.addNumericField("Look_ahead_cache:", look_ahead_cache, 0, 6, "layers");
1507 int autosaving_interval = getProperty("autosaving_interval", 10); // default: every 10 minutes
1508 gd.addNumericField("Autosave every:", autosaving_interval, 0, 6, "minutes");
1509 int n_mipmap_threads = getProperty("n_mipmap_threads", 1);
1510 gd.addSlider("Number of threads for mipmaps", 1, n_mipmap_threads, n_mipmap_threads);
1511 int meshResolution = getProperty("mesh_resolution", 32);
1512 gd.addSlider("Default mesh resolution for images", 1, 512, meshResolution);
1514 gd.showDialog();
1516 if (gd.wasCanceled()) return;
1517 setLinkProp(link_labels, gd.getNextBoolean(), DLabel.class);
1519 boolean nolink_segmentations2 = gd.getNextBoolean();
1520 if (nolink_segmentations) {
1521 if (!nolink_segmentations2) ht_props.remove("segmentations_nolinks");
1522 } else if (nolink_segmentations2) ht_props.put("segmentations_nolinks", "true");
1524 if (adjustProp("dissector_zoom", dissector_zoom, gd.getNextBoolean())) {
1525 Display.repaint(layer_set); // TODO: should repaint nested LayerSets as well
1527 this.mipmaps_mode = Loader.getMipMapModeIndex(gd.getNextChoice());
1529 final int new_mipmap_format = gd.getNextChoiceIndex();
1530 final int old_mipmap_format = loader.getMipMapFormat();
1531 if (new_mipmap_format != old_mipmap_format) {
1532 YesNoDialog yn = new YesNoDialog("MipMaps format", "Changing mipmaps format to '" + FSLoader.MIPMAP_FORMATS[new_mipmap_format] + "'requires regenerating all mipmaps. Proceed?");
1533 if (yn.yesPressed()) {
1534 if (loader.setMipMapFormat(new_mipmap_format)) {
1535 loader.updateMipMapsFormat(old_mipmap_format, new_mipmap_format);
1540 boolean layer_mipmaps2 = gd.getNextBoolean();
1541 if (adjustProp("layer_mipmaps", layer_mipmaps, layer_mipmaps2)) {
1542 if (layer_mipmaps && !layer_mipmaps2) {
1543 // TODO
1544 // 1 - ask first
1545 // 2 - remove all existing images from layer.mipmaps folder
1546 } else if (!layer_mipmaps && layer_mipmaps2) {
1547 // TODO
1548 // 1 - ask first
1549 // 2 - create de novo all layer mipmaps in a background task
1552 adjustProp("keep_mipmaps", keep_mipmaps, gd.getNextBoolean());
1553 Utils.log2("keep_mipmaps: " + getBooleanProperty("keep_mipmaps"));
1555 bucket_side = (int)gd.getNextNumber();
1556 if (bucket_side > Bucket.MIN_BUCKET_SIZE) {
1557 setProperty("bucket_side", Integer.toString(bucket_side));
1558 layer_set.recreateBuckets(true);
1560 adjustProp("no_shutdown_hook", no_shutdown_hook, gd.getNextBoolean());
1561 n_undo_steps = (int)gd.getNextNumber();
1562 if (n_undo_steps < 0) n_undo_steps = 0;
1563 setProperty("n_undo_steps", Integer.toString(n_undo_steps));
1564 adjustProp("flood_fill_to_image_edge", flood_fill_to_image_edge, gd.getNextBoolean());
1565 double d_look_ahead_cache = gd.getNextNumber();
1566 if (!Double.isNaN(d_look_ahead_cache) && d_look_ahead_cache >= 0) {
1567 setProperty("look_ahead_cache", Integer.toString((int)d_look_ahead_cache));
1568 if (0 == d_look_ahead_cache) {
1569 Display.clearColumnScreenshots(this.layer_set);
1570 } else {
1571 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.");
1573 } else {
1574 Utils.log2("Ignoring invalid 'look ahead cache' value " + d_look_ahead_cache);
1576 double autosaving_interval2 = gd.getNextNumber();
1577 if (((int)(autosaving_interval2)) == autosaving_interval) {
1578 // do nothing
1579 } else if (autosaving_interval2 < 0 || Double.isNaN(autosaving_interval)) {
1580 Utils.log("IGNORING invalid autosaving interval: " + autosaving_interval2);
1581 } else {
1582 setProperty("autosaving_interval", Integer.toString((int)autosaving_interval2));
1583 restartAutosaving();
1585 int n_mipmap_threads2 = (int)Math.max(1, gd.getNextNumber());
1586 if (n_mipmap_threads != n_mipmap_threads2) {
1587 setProperty("n_mipmap_threads", Integer.toString(n_mipmap_threads2));
1588 // WARNING: this does it for a static service, affecting all projects!
1589 FSLoader.restartMipMapThreads(n_mipmap_threads2);
1591 int meshResolution2 = (int)gd.getNextNumber();
1592 if (meshResolution != meshResolution2) {
1593 if (meshResolution2 > 0) {
1594 setProperty("mesh_resolution", Integer.toString(meshResolution2));
1595 } else {
1596 Utils.log("WARNING: ignoring invalid mesh resolution value " + meshResolution2);
1601 /** Return the Universal Near-Unique Id of this project, which may be null for non-FSLoader projects. */
1602 public String getUNUId() {
1603 return loader.getUNUId();
1606 /** Removes an object from this Project. */
1607 public final boolean remove(final Displayable d) {
1608 final Set<Displayable> s = new HashSet<Displayable>();
1609 s.add(d);
1610 return removeAll(s);
1613 /** Calls Project.removeAll(col, null) */
1614 public final boolean removeAll(final Set<Displayable> col) {
1615 return removeAll(col, null);
1617 /** Remove any set of Displayable objects from the Layer, LayerSet and Project Tree as necessary.
1618 * ASSUMES there aren't any nested LayerSet objects in @param col. */
1619 public final boolean removeAll(final Set<Displayable> col, final DefaultMutableTreeNode top_node) {
1620 // 0. Sort into Displayable and ZDisplayable
1621 final Set<ZDisplayable> zds = new HashSet<ZDisplayable>();
1622 final List<Displayable> ds = new ArrayList<Displayable>();
1623 for (final Displayable d : col) {
1624 if (d instanceof ZDisplayable) {
1625 zds.add((ZDisplayable)d);
1626 } else {
1627 ds.add(d);
1631 // Displayable:
1632 // 1. First the Profile from the Project Tree, one by one,
1633 // while creating a map of Layer vs Displayable list to remove in that layer:
1634 final HashMap<Layer,Set<Displayable>> ml = new HashMap<Layer,Set<Displayable>>();
1635 for (final Iterator<Displayable> it = ds.iterator(); it.hasNext(); ) {
1636 final Displayable d = it.next();
1637 if (d.getClass() == Profile.class) {
1638 if (!project_tree.remove(false, findProjectThing(d), null)) { // like Profile.remove2
1639 Utils.log("Could NOT delete " + d);
1640 continue;
1642 it.remove(); // remove the Profile
1643 continue;
1645 // The map of Layer vs Displayable list
1646 Set<Displayable> l = ml.get(d.getLayer());
1647 if (null == l) {
1648 l = new HashSet<Displayable>();
1649 ml.put(d.getLayer(), l);
1651 l.add(d);
1653 // 2. Then the rest, in bulk:
1654 if (ml.size() > 0) {
1655 for (final Map.Entry<Layer,Set<Displayable>> e : ml.entrySet()) {
1656 e.getKey().removeAll(e.getValue());
1659 // 3. Stacks
1660 if (zds.size() > 0) {
1661 final Set<ZDisplayable> stacks = new HashSet<ZDisplayable>();
1662 for (final Iterator<ZDisplayable> it = zds.iterator(); it.hasNext(); ) {
1663 final ZDisplayable zd = it.next();
1664 if (zd.getClass() == Stack.class) {
1665 it.remove();
1666 stacks.add(zd);
1669 layer_set.removeAll(stacks);
1672 // 4. ZDisplayable: bulk removal
1673 if (zds.size() > 0) {
1674 // 1. From the Project Tree:
1675 Set<Displayable> not_removed = project_tree.remove(zds, top_node);
1676 // 2. Then only those successfully removed, from the LayerSet:
1677 zds.removeAll(not_removed);
1678 layer_set.removeAll(zds);
1681 // TODO
1682 return true;
1686 /** For undo purposes. */
1687 public void resetRootProjectThing(final ProjectThing pt, final HashMap<Thing,Boolean> ptree_exp) {
1688 this.root_pt = pt;
1689 project_tree.reset(ptree_exp);
1691 /** For undo purposes. */
1692 public void resetRootTemplateThing(final TemplateThing tt, final HashMap<Thing,Boolean> ttree_exp) {
1693 this.root_tt = tt;
1694 template_tree.reset(ttree_exp);
1696 /** For undo purposes. */
1697 public void resetRootLayerThing(final LayerThing lt, final HashMap<Thing,Boolean> ltree_exp) {
1698 this.root_lt = lt;
1699 layer_tree.reset(ltree_exp);
1702 public TemplateThing getRootTemplateThing() {
1703 return root_tt;
1706 public LayerThing getRootLayerThing() {
1707 return root_lt;
1710 public Bureaucrat saveTask(final String command) {
1711 return Bureaucrat.createAndStart(new Worker.Task("Saving") {
1712 public void exec() {
1713 if (command.equals("Save")) {
1714 save();
1715 } else if (command.equals("Save as...")) {
1716 XMLOptions options = new XMLOptions();
1717 options.overwriteXMLFile = false;
1718 options.export_images = false;
1719 options.include_coordinate_transform = true;
1720 options.patches_dir = null;
1721 // Will open a file dialog
1722 loader.saveAs(project, options);
1723 restartAutosaving();
1725 } else if (command.equals("Save as... without coordinate transforms")) {
1726 YesNoDialog yn = new YesNoDialog("WARNING",
1727 "You are about to save an XML file that lacks the information for the coordinate transforms of each image.\n"
1728 + "These transforms are referred to with the attribute 'ct_id' of each 't2_patch' entry in the XML document,\n"
1729 + "and the data for the transform is stored in an individual file under the folder 'trakem2.cts/'.\n"
1730 + " \n"
1731 + "It is advised to keep a complete XML file with all coordinate transforms included along with this new copy.\n"
1732 + "Please check NOW that you have such a complete XML copy.\n"
1733 + " \n"
1734 + "Proceed?");
1735 if (!yn.yesPressed()) return;
1736 saveWithoutCoordinateTransforms();
1738 } else if (command.equals("Delete stale files...")) {
1739 setTaskName("Deleting stale files");
1740 GenericDialog gd = new GenericDialog("Delete stale files");
1741 gd.addMessage(
1742 "You are about to remove all files under the folder 'trakem2.cts/' which are not referred to from the\n"
1743 + "currently loaded project. If you have sibling XML files whose 't2_patch' entries (the images) refer,\n"
1744 + "via 'ct_id' attributes, to coordinate transforms in 'trakem2.cts/' that this current XML doesn't,\n"
1745 + "they may be LOST FOREVER. Unless you have a version of the XML file with the coordinate transforms\n"
1746 + "written in it, as can be obtained by using the 'Project - Save' command.\n"
1747 + " \n"
1748 + "The same is true for the .zip files that store alpha masks, under folder 'trakem2.masks/'\n"
1749 + "and which are referred to from the 'alpha_mask_id' attribute of 't2_patch' entries.\n"
1750 + " \n"
1751 + "Do you have such complete XML file? Check *NOW*.\n"
1752 + " \n"
1753 + "Proceed with deleting:"
1755 gd.addCheckbox("Delete stale coordinate transform files", true);
1756 gd.addCheckbox("Delete stale alpha mask files", true);
1757 gd.showDialog();
1758 if (gd.wasCanceled()) return;
1759 project.getLoader().deleteStaleFiles(gd.getNextBoolean(), gd.getNextBoolean());
1762 }, project);
1765 /** The mode (aka algorithmic approach) used to generate mipmaps, which defaults to {@link Loader#DEFAULT_MIPMAPS_MODE}. */
1766 public int getMipMapsMode() {
1767 return this.mipmaps_mode;
1770 /** @see #getMipMapsMode() */
1771 public void setMipMapsMode(int mode) {
1772 this.mipmaps_mode = mode;