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.
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
;
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
;
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
{
101 //UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
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
> {
119 PlugInSource(String menu
, Class
<?
> c
, String title
) {
124 public int compareTo(PlugInSource ob
) {
125 return ob
.title
.compareTo(this.title
);
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")) {
148 if (entry
== null) continue;
150 BufferedReader br
= new BufferedReader(new InputStreamReader(jar
.getInputStream(entry
)));
153 String line
= br
.readLine();
154 if (null == line
) break;
155 if (line
.startsWith("#")) continue;
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();
169 Class
.forName(classname
);
170 } catch (ClassNotFoundException cnfe
) {
171 Utils
.log2("TPlugIn class not found: " + classname
);
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();
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
);
190 } catch (Throwable t
) {
191 Utils
.log("ERROR while parsing TrakEM2 plugins:");
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
) {
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() + "]");
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());
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
) {
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
) {
279 ControlWindow
.getInstance(); // init
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;
295 this.autosaving
= FSLoader
.autosaver
.scheduleWithFixedDelay(new Runnable() {
298 if (loader
.hasChanges()) {
299 Bureaucrat
.createAndStart(new Worker
.Task("auto-saving") {
304 }, Project
.this).join();
306 } catch (Throwable e
) {
307 Utils
.log("*** Autosaver failed:");
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
;
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;
330 DBLoader loader
= new DBLoader();
331 // check connection settings
332 if (!loader
.isReady()) return null;
334 if (!loader
.isConnected()) {
335 Utils
.showMessage("Can't talk to database.");
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;
347 if (!loader
.isConnected()) {
348 Utils
.showMessage("Can't talk to database.");
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).");
359 Project project
= null;
360 if (0 == projects
.length
) {
361 Utils
.showMessage("No projects in this database.");
364 } else if (1 == projects
.length
) {
365 project
= projects
[0];
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]);
376 if (gd
.wasCanceled()) {
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.");
391 // now, open the selected project
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.");
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.
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.");
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;
433 root_layer_thing
= loader
.getRootLayerThing(project
, project
.root_pt
, Project
.layer_set_template
, Project
.layer_template
);
434 if (null == root_layer_thing
) {
436 Utils
.showMessage("Could not retrieve the root layer thing.");
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
) {
444 Utils
.showMessage("Could not retrieve the root layer set.");
447 project
.layer_set
.setup(); // set the active layer to each ZDisplayable
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.");
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:
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;
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.");
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
);
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();
536 } catch (Exception e
) {
538 if (null != loader
) loader
.destroy();
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
);
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
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
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()));
598 Utils
.log2("Can't find node for " + pt
);
600 ht_exp
.put(new TreePath(nd
.getPath()), expanded
);
603 project
.project_tree
.updateUILater(); // very important!!
604 } catch (Exception e
) {
607 // open any stored displays
609 final Bureaucrat burro
= Display
.openLater();
611 final Runnable ru
= new Runnable() {
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");
622 setPriority(Thread
.NORM_PRIORITY
);
623 // avoiding "can't call invokeAndWait from the EventDispatch thread" error
625 javax
.swing
.SwingUtilities
.invokeAndWait(ru
);
626 } catch (Exception e
) {
627 Utils
.log2("ERROR: " + e
);
631 // SO: WAIT TILL THE END OF TIME!
632 new Thread() { public void run() {
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");
639 project
.getTemplateTree().updateUILater(); // repainting to fix gross errors in tree rendering
640 project
.getProjectTree().updateUILater(); // idem
641 } catch (Exception ie
) {}
644 // help the helpless users
645 Display
.createDisplay(project
, project
.layer_set
.getLayer(0));
649 project
.restartAutosaving();
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.
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
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'
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
) {
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.
709 al_open_projects
.add(project
);
715 public void setTempLoader(Loader loader
) {
716 if (null == this.loader
) {
717 this.loader
= loader
;
719 Utils
.log2("Project.setTempLoader: already have one.");
723 public final Loader
getLoader() {
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();
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();
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
) {
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()) {
777 Utils
.log2("WARNING: closing project '" + title
+ "' with unsaved changes.");
781 if (null != autosaving
) autosaving
.cancel(true);
782 } catch (Throwable t
) {}
783 al_open_projects
.remove(this);
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
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
798 Search
.removeTabs(this);
799 synchronized (ptcache
) { ptcache
.clear(); }
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();
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();
821 public void setTitle(String title
) {
822 if (null == title
) return;
824 ControlWindow
.updateTitle(this);
825 loader
.updateInDatabase(this, "title");
828 public String
toString() {
829 if (null == title
|| title
.equals("Project")) {
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."); }
837 public String
getTitle() {
841 public TemplateTree
getTemplateTree() {
842 return template_tree
;
845 public LayerTree
getLayerTree() {
849 public ProjectTree
getProjectTree() {
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
886 // just the name, for the abstract ones
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
)
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
) {
936 Utils
.log2("Project.removeProjectThing: levels must be zero or above.");
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
);
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.
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
);
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
) {
1004 synchronized (ptcache
) { pt
= ptcache
.get(ob
); }
1006 pt
= (ProjectThing
) root_pt
.findChild(ob
);
1007 if (null != ob
) synchronized (ptcache
) { ptcache
.put(ob
, pt
); }
1012 public void decache(final Object ob
) {
1013 synchronized (ptcache
) {
1018 public ProjectThing
getRootProjectThing() {
1022 public LayerSet
getRootLayerSet() {
1026 /** Returns the title of the enclosing abstract node in the ProjectTree.*/
1027 public String
getParentTitle(final Displayable d
) {
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())) {
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;
1088 //title = type + "/" + title;
1089 parent
= (ProjectThing
)parent
.getParent();
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
;
1116 parent
= (ProjectThing
)parent
.getParent();
1118 // if nothing found, prepend the type
1119 if ('#' == title
.charAt(0)) title
= Project
.getName(d
.getClass()) + " " + 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);
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");
1162 for (Iterator it = ht_unique_tt.keySet().iterator(); it.hasNext(); ) {
1163 Utils.log2("class: " + it.next().getClass().getName());
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
);
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
);
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!");
1200 ht_unique_tt
.put(new_type
, ht_unique_tt
.remove(old_type
));
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.
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);
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
);
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
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
;
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
) {
1355 // The order matters.
1356 final Project pr
= new Project(new FSLoader(this.getLoader().getStorageFolder()));
1359 pr
.title
= this.title
;
1360 pr
.ht_props
.putAll(this.ht_props
);
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.
1387 al_open_projects
.add(pr
);
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();
1403 } catch (Exception e
) { e
.printStackTrace(); }
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
) {
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
;
1434 } catch (NumberFormatException nfe
) {
1437 return default_value
;
1440 public int getProperty(final String key
, final int default_value
) {
1442 final String s
= ht_props
.get(key
);
1443 if (null == s
) return default_value
;
1444 return Integer
.parseInt(s
);
1445 } catch (NumberFormatException 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
);
1464 private final void setLinkProp(final boolean before
, final boolean after
, final Class
<?
> c
) {
1466 if (!after
) ht_props
.remove(Project
.getName(c
).toLowerCase()+"_nolinks");
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
) {
1475 if (!after
) ht_props
.remove(prop
);
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
);
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
) {
1545 // 2 - remove all existing images from layer.mipmaps folder
1546 } else if (!layer_mipmaps
&& layer_mipmaps2
) {
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
);
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.");
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
) {
1579 } else if (autosaving_interval2
< 0 || Double
.isNaN(autosaving_interval
)) {
1580 Utils
.log("IGNORING invalid autosaving interval: " + autosaving_interval2
);
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
));
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
>();
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
);
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
);
1642 it
.remove(); // remove the Profile
1645 // The map of Layer vs Displayable list
1646 Set
<Displayable
> l
= ml
.get(d
.getLayer());
1648 l
= new HashSet
<Displayable
>();
1649 ml
.put(d
.getLayer(), l
);
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());
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) {
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
);
1686 /** For undo purposes. */
1687 public void resetRootProjectThing(final ProjectThing pt
, final HashMap
<Thing
,Boolean
> ptree_exp
) {
1689 project_tree
.reset(ptree_exp
);
1691 /** For undo purposes. */
1692 public void resetRootTemplateThing(final TemplateThing tt
, final HashMap
<Thing
,Boolean
> ttree_exp
) {
1694 template_tree
.reset(ttree_exp
);
1696 /** For undo purposes. */
1697 public void resetRootLayerThing(final LayerThing lt
, final HashMap
<Thing
,Boolean
> ltree_exp
) {
1699 layer_tree
.reset(ltree_exp
);
1702 public TemplateThing
getRootTemplateThing() {
1706 public LayerThing
getRootLayerThing() {
1710 public Bureaucrat
saveTask(final String command
) {
1711 return Bureaucrat
.createAndStart(new Worker
.Task("Saving") {
1712 public void exec() {
1713 if (command
.equals("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"
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"
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");
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"
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"
1751 + "Do you have such complete XML file? Check *NOW*.\n"
1753 + "Proceed with deleting:"
1755 gd
.addCheckbox("Delete stale coordinate transform files", true);
1756 gd
.addCheckbox("Delete stale alpha mask files", true);
1758 if (gd
.wasCanceled()) return;
1759 project
.getLoader().deleteStaleFiles(gd
.getNextBoolean(), gd
.getNextBoolean());
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
;