fixed improper generic parameter use in Tree.duplicateAs > Map, necessary
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / display / Display3D.java
blob39771bd3b52ee2235dbe38bc2343e7b1d490bb8b
1 package ini.trakem2.display;
3 import java.awt.Color;
4 import java.awt.Cursor;
5 import java.awt.event.WindowAdapter;
6 import java.awt.event.WindowEvent;
7 import java.io.File;
8 import java.util.ArrayList;
9 import java.util.Arrays;
10 import java.util.Collection;
11 import java.util.Collections;
12 import java.util.HashMap;
13 import java.util.HashSet;
14 import java.util.Hashtable;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.Vector;
19 import java.util.concurrent.Callable;
20 import java.util.concurrent.ExecutorService;
21 import java.util.concurrent.Executors;
22 import java.util.concurrent.Future;
23 import java.util.concurrent.FutureTask;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.atomic.AtomicInteger;
28 import org.scijava.java3d.PolygonAttributes;
29 import org.scijava.java3d.Transform3D;
30 import org.scijava.java3d.View;
31 import org.scijava.vecmath.Color3f;
32 import org.scijava.vecmath.Point3f;
34 import customnode.CustomLineMesh;
35 import customnode.CustomMesh;
36 import customnode.CustomMultiMesh;
37 import customnode.CustomTriangleMesh;
38 import ij.ImagePlus;
39 import ij.gui.GenericDialog;
40 import ij.measure.Calibration;
41 import ij3d.Content;
42 import ij3d.Image3DUniverse;
43 import ij3d.ImageWindow3D;
44 import ij3d.UniverseListener;
45 import ini.trakem2.display.d3d.ControlClickBehavior;
46 import ini.trakem2.display.d3d.Display3DGUI;
47 import ini.trakem2.imaging.PatchStack;
48 import ini.trakem2.tree.ProjectThing;
49 import ini.trakem2.utils.IJError;
50 import ini.trakem2.utils.Utils;
51 import ini.trakem2.vector.VectorString3D;
54 /** One Display3D instance for each LayerSet (maximum). */
55 public final class Display3D {
57 /** Table of LayerSet and Display3D - since there is a one to one relationship. */
58 static private Hashtable<LayerSet,Display3D> ht_layer_sets = new Hashtable<LayerSet,Display3D>();
59 /**Control calls to new Display3D. */
60 static private Object htlock = new Object();
62 /** The sky will fall on your head if you modify any of the objects contained in this table -- which is a copy of the original, but the objects are the originals. */
63 static public Hashtable<LayerSet,Display3D> getMasterTable() {
64 return new Hashtable<LayerSet,Display3D>(ht_layer_sets);
67 /** Table of ProjectThing keys versus names of Content objects in the universe. */
68 private Map<ProjectThing,String> ht_pt_meshes = Collections.synchronizedMap(new HashMap<ProjectThing,String>());
70 private Image3DUniverse universe;
72 private LayerSet layer_set;
73 /** The dimensions of the LayerSet in 2D. */
74 private double width, height;
75 private int resample = -1; // unset
76 static private final int DEFAULT_RESAMPLE = 4;
77 /** If the LayerSet dimensions are too large, then limit to max 2048 for width or height and setup a scale.*/
78 private final double scale = 1.0; // OBSOLETE: meshes are now generated with imglib ShapeList images.
81 // To fork away from the EventDispatchThread
82 static private ExecutorService launchers = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-launchers");
84 // To build meshes, or edit them
85 private ExecutorService executors = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-executors");
88 static private KeyAdapter ka = new KeyAdapter() {
89 public void keyPressed(KeyEvent ke) {
90 // F1 .. F12 keys to set tools
91 ProjectToolbar.keyPressed(ke);
96 /** Defaults to parallel projection. */
97 private Display3D(final LayerSet ls) {
98 this.layer_set = ls;
99 this.width = ls.getLayerWidth();
100 this.height = ls.getLayerHeight();
101 this.universe = new Image3DUniverse(512, 512); // size of the initial canvas, not the universe itself
102 this.universe.getViewer().getView().setProjectionPolicy(View.PERSPECTIVE_PROJECTION); // (View.PERSPECTIVE_PROJECTION);
104 //this.universe.show();
106 final Display3DGUI gui = new Display3DGUI(this.universe);
107 final ImageWindow3D win = gui.init();
108 this.universe.init(win);
109 win.pack();
110 win.setVisible(true);
112 this.universe.getWindow().addWindowListener(new IW3DListener(this, ls));
113 this.universe.getWindow().setTitle(ls.getProject().toString() + " -- 3D Viewer");
114 // it ignores the listeners:
115 //preaddKeyListener(this.universe.getWindow(), ka);
116 //preaddKeyListener(this.universe.getWindow().getCanvas(), ka);
118 // register
119 Display3D.ht_layer_sets.put(ls, this);
121 // Add a behavior to catch control + mouse-click on
122 // objects in the 3D viewer and centre the front Display
123 // on that point:
124 this.universe.addInteractiveBehavior(new ControlClickBehavior(universe, ls));
126 this.universe.addUniverseListener(new UniverseListener() {
127 @Override
128 public void universeClosed() {
129 synchronized (ht_pt_meshes) {
130 ht_pt_meshes.clear();
133 @Override
134 public void transformationUpdated(final View arg0) {
136 @Override
137 public void transformationStarted(final View arg0) {
139 @Override
140 public void transformationFinished(final View arg0) {
142 @Override
143 public void contentSelected(final Content arg0) {
144 // TODO could select in TrakEM2's Display
146 @Override
147 public void contentRemoved(final Content arg0) {
148 final String name = arg0.getName();
149 synchronized (ht_pt_meshes) {
150 for (final Iterator<Map.Entry<ProjectThing,String>> it = ht_pt_meshes.entrySet().iterator(); it.hasNext(); ) {
151 if (name.equals(it.next().getValue())) {
152 it.remove();
153 break;
158 @Override
159 public void contentChanged(final Content arg0) {
161 @Override
162 public void contentAdded(final Content arg0) {
164 @Override
165 public void canvasResized() {
171 private void preaddKeyListener(Component c, KeyListener kl) {
172 KeyListener[] all = c.getKeyListeners();
173 if (null != all) {
174 for (KeyListener k : all) c.removeKeyListener(k);
176 c.addKeyListener(kl);
177 if (null != all) {
178 for (KeyListener k : all) c.addKeyListener(k);
183 public Image3DUniverse getUniverse() {
184 return universe;
187 /* Take a snapshot know-it-all mode. Each Transform3D given as argument gets assigned to the (nearly) homonimous TransformGroup, which have the following relationships:
189 * scaleTG contains rotationsTG
190 * rotationsTG contains translateTG
191 * translateTG contains centerTG
192 * centerTG contains the whole scene, with all meshes, etc.
194 * Any null arguments imply the current transform in the open Display3D.
196 * By default, a newly created Display3D has the scale and center transforms modified to make the scene fit nicely centered (and a bit scaled down) in the given Display3D window. The translate and rotate transforms are set to identity.
198 * The TransformGroup instances may be reached like this:
200 * LayerSet layer_set = Display.getFrontLayer().getParent();
201 * Display3D d3d = Display3D.getDisplay(layer_set);
202 * TransformGroup scaleTG = d3d.getUniverse().getGlobalScale();
203 * TransformGroup rotationsTG = d3d.getUniverse().getGlobalRotate();
204 * TransformGroup translateTG = d3d.getUniverse().getGlobalTranslate();
205 * TransformGroup centerTG = d3d.getUniverse().getCenterTG();
207 * ... and the Transform3D from each may be read out indirectly like this:
209 * Transform3D t_scale = new Transform3D();
210 * scaleTG.getTransform(t_scale);
211 * ...
213 * WARNING: if your java3d setup does not support offscreen rendering, the Display3D window will be brought to the front and a screen snapshot cropped to it to perform the snapshot capture. Don't cover the Display3D window with any other windows (not even an screen saver).
216 /*public ImagePlus makeSnapshot(final Transform3D scale, final Transform3D rotate, final Transform3D translate, final Transform3D center) {
217 return universe.makeSnapshot(scale, rotate, translate, center);
220 /** Uses current scaling, translation and centering transforms! */
221 /*public ImagePlus makeSnapshotXY() { // aka posterior
222 // default view
223 return universe.makeSnapshot(null, new Transform3D(), null, null);
225 /** Uses current scaling, translation and centering transforms! */
226 /*public ImagePlus makeSnapshotXZ() { // aka dorsal
227 Transform3D rot1 = new Transform3D();
228 rot1.rotZ(-Math.PI/2);
229 Transform3D rot2 = new Transform3D();
230 rot2.rotX(Math.PI/2);
231 rot1.mul(rot2);
232 return universe.makeSnapshot(null, rot1, null, null);
235 /** Uses current scaling, translation and centering transforms! */
237 public ImagePlus makeSnapshotYZ() { // aka lateral
238 Transform3D rot = new Transform3D();
239 rot.rotY(Math.PI/2);
240 return universe.makeSnapshot(null, rot, null, null);
244 public ImagePlus makeSnapshotZX() { // aka frontal
245 Transform3D rot = new Transform3D();
246 rot.rotX(-Math.PI/2);
247 return universe.makeSnapshot(null, rot, null, null);
251 /** Uses current scaling, translation and centering transforms! Opposite side of XZ. */
253 public ImagePlus makeSnapshotXZOpp() {
254 Transform3D rot1 = new Transform3D();
255 rot1.rotX(-Math.PI/2); // 90 degrees clockwise
256 Transform3D rot2 = new Transform3D();
257 rot2.rotY(Math.PI); // 180 degrees around Y, to the other side.
258 rot1.mul(rot2);
259 return universe.makeSnapshot(null, rot1, null, null);
262 private class IW3DListener extends WindowAdapter {
263 private Display3D d3d;
264 private LayerSet ls;
265 IW3DListener(final Display3D d3d, final LayerSet ls) {
266 this.d3d = d3d;
267 this.ls = ls;
269 @Override
270 public void windowClosing(final WindowEvent we) {
271 //Utils.log2("Display3D.windowClosing");
272 d3d.executors.shutdownNow();
273 /*Object ob =*/ ht_layer_sets.remove(ls);
274 /*if (null != ob) {
275 Utils.log2("Removed Display3D from table for LayerSet " + ls);
278 @Override
279 public void windowClosed(final WindowEvent we) {
280 //Utils.log2("Display3D.windowClosed");
281 ht_layer_sets.remove(ls);
285 static private boolean check_j3d = true;
286 static private boolean has_j3d_3dviewer = false;
288 static private boolean hasLibs() {
289 if (check_j3d) {
290 check_j3d = false;
291 try {
292 Class.forName("javax.vecmath.Point3f");
293 has_j3d_3dviewer = true;
294 } catch (final ClassNotFoundException cnfe) {
295 Utils.log("Java 3D not installed.");
296 has_j3d_3dviewer = false;
297 return false;
299 try {
300 Class.forName("ij3d.ImageWindow3D");
301 has_j3d_3dviewer = true;
302 } catch (final ClassNotFoundException cnfe) {
303 Utils.log("3D Viewer not installed.");
304 has_j3d_3dviewer = false;
305 return false;
308 return has_j3d_3dviewer;
311 /** Get an existing Display3D for the given LayerSet, or create a new one for it (and cache it). */
312 static public Display3D get(final LayerSet ls) {
313 synchronized (htlock) {
314 try {
315 // test:
316 if (!hasLibs()) return null;
318 final Display3D d3d = ht_layer_sets.get(ls);
319 if (null != d3d) return d3d;
320 // Else, new:
321 final boolean[] done = new boolean[]{false};
322 javax.swing.SwingUtilities.invokeAndWait(new Runnable() { @Override
323 public void run() {
324 ht_layer_sets.put(ls, new Display3D(ls));
325 done[0] = true;
326 }});
327 // wait to avoid crashes in amd64
328 // try { Thread.sleep(500); } catch (Exception e) {}
329 while (!done[0]) {
330 try { Thread.sleep(10); } catch (final Exception e) {}
332 return ht_layer_sets.get(ls);
333 } catch (final Exception e) {
334 IJError.print(e);
337 return null;
340 /** Get the Display3D instance that exists for the given LayerSet, if any. */
341 static public Display3D getDisplay(final LayerSet ls) {
342 return ht_layer_sets.get(ls);
345 static public void setWaitingCursor() {
346 Utils.invokeLater(new Runnable() { @Override
347 public void run() {
348 for (final Display3D d3d : ht_layer_sets.values()) {
349 d3d.universe.getWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
351 }});
354 static public void doneWaiting() {
355 Utils.invokeLater(new Runnable() { @Override
356 public void run() {
357 for (final Display3D d3d : ht_layer_sets.values()) {
358 d3d.universe.getWindow().setCursor(Cursor.getDefaultCursor());
360 }});
363 static public Future<Vector<Future<Content>>> show(final ProjectThing pt) {
364 return show(pt, false, -1);
367 static public void showAndResetView(final ProjectThing pt) {
368 launchers.submit(new Runnable() {
369 @Override
370 public void run() {
371 // wait until done
372 final Future<Vector<Future<Content>>> fu = show(pt, true, -1);
373 Vector<Future<Content>> vc;
374 try {
375 vc = fu.get(); // wait until done
376 } catch (final Exception e) {
377 IJError.print(e);
378 return;
380 for (final Future<Content> fc : vc) {
381 try {
382 final Content c = fc.get();
383 if (null == c) continue;
384 final ArrayList<Display3D> d3ds = new ArrayList<Display3D>();
385 synchronized (ht_layer_sets) {
386 d3ds.addAll(ht_layer_sets.values());
388 /* // Disabled, it's annoying
389 for (Display3D d3d : d3ds) {
390 synchronized (d3d) {
391 if (d3d.universe.getContents().contains(c)) {
392 d3d.universe.resetView(); // reset the absolute center
393 d3d.universe.adjustView(); // zoom out to bring all elements in universe within view
397 } catch (final Exception e) {
398 IJError.print(e);
401 Utils.logAll("Reset 3D view if not within field of view!");
406 /** Scan the ProjectThing children and assign the renderable ones to an existing Display3D for their LayerSet, or open a new one. If true == wait && -1 != resample, then the method returns only when the mesh/es have been added. */
407 static public Future<Vector<Future<Content>>> show(final ProjectThing pt, final boolean wait, final int resample) {
408 if (null == pt) return null;
410 final Future<Vector<Future<Content>>> fu = launchers.submit(new Callable<Vector<Future<Content>>>() {
411 @Override
412 public Vector<Future<Content>> call() {
414 // Scan the given ProjectThing for 3D-viewable items
415 // So: find arealist, pipe, ball, and profile_list types
416 final HashSet<ProjectThing> hs = pt.findBasicTypeChildren();
417 if (null == hs || 0 == hs.size()) {
418 Utils.logAll("Node " + pt + " does not contain any 3D-displayable children");
419 return null;
422 // Remove profile if it lives under a profile_list
423 for (final Iterator<ProjectThing> it = hs.iterator(); it.hasNext(); ) {
424 final ProjectThing pt = it.next();
425 if (null != pt.getObject() && pt.getObject().getClass() == Profile.class && pt.getParent().getType().equals("profile_list")) {
426 it.remove();
430 setWaitingCursor();
432 // Start new scheduler to publish/add meshes to the 3D Viewer every 5 seconds and when done.
433 final Hashtable<Display3D,Vector<Content>> contents = new Hashtable<Display3D,Vector<Content>>();
434 final ScheduledExecutorService updater = Executors.newScheduledThreadPool(1);
435 final AtomicInteger counter = new AtomicInteger();
436 updater.scheduleWithFixedDelay(new Runnable() {
437 @Override
438 public void run() {
439 // Obtain a copy of the contents queue
440 final HashMap<Display3D,Vector<Content>> m = new HashMap<Display3D,Vector<Content>>();
441 synchronized (contents) {
442 m.putAll(contents);
443 contents.clear();
445 if (m.isEmpty()) return;
446 // Add all to the corresponding Display3D
447 for (final Map.Entry<Display3D,Vector<Content>> e : m.entrySet()) {
448 e.getKey().universe.addContentLater(e.getValue());
449 counter.getAndAdd(e.getValue().size());
451 Utils.showStatus(new StringBuilder("Rendered ").append(counter.get()).append('/').append(hs.size()).toString());
453 }, 100, 4000, TimeUnit.MILLISECONDS);
455 // A list of all generated Content objects
456 final Vector<Future<Content>> list = new Vector<Future<Content>>();
458 for (final Iterator<ProjectThing> it = hs.iterator(); it.hasNext(); ) {
459 // obtain the Displayable object under the node
460 final ProjectThing child = it.next();
462 final Object obc = child.getObject();
463 final Displayable displ = obc.getClass().equals(String.class) ? null : (Displayable)obc;
464 if (null != displ) {
465 if (displ.getClass().equals(Profile.class)) {
466 //Utils.log("Display3D can't handle Bezier profiles at the moment.");
467 // handled by profile_list Thing
468 continue;
470 if (!displ.isVisible()) {
471 Utils.log("Skipping non-visible node " + displ);
472 continue;
475 // obtain the containing LayerSet
476 final Display3D d3d;
477 if (null != displ) d3d = Display3D.get(displ.getLayerSet());
478 else if (child.getType().equals("profile_list")) {
479 final ArrayList<ProjectThing> al_children = child.getChildren();
480 if (null == al_children || 0 == al_children.size()) continue;
481 // else, get the first Profile and get its LayerSet
482 d3d = Display3D.get(((Displayable)((ProjectThing)al_children.get(0)).getObject()).getLayerSet());
483 } else {
484 Utils.log("Don't know what to do with node " + child);
485 d3d = null;
487 if (null == d3d) {
488 Utils.log("Could not get a proper 3D display for node " + displ);
489 return null; // java3D not installed most likely
492 boolean already;
493 synchronized (d3d.ht_pt_meshes) {
494 already = d3d.ht_pt_meshes.containsKey(child);
496 if (already) {
497 if (child.getObject() instanceof ZDisplayable) {
498 Utils.log("Updating 3D view of " + child.getObject());
499 } else {
500 Utils.log("Updating 3D view of " + child);
504 list.add(d3d.executors.submit(new Callable<Content>() {
505 @Override
506 public Content call() {
507 Content c = null;
508 try {
509 c = d3d.createMesh(child, displ, resample).call();
510 Vector<Content> vc;
511 synchronized (contents) {
512 vc = contents.get(d3d);
513 if (null == vc) vc = new Vector<Content>();
514 contents.put(d3d, vc);
516 vc.add(c);
517 } catch (final Exception e) {
518 IJError.print(e);
520 return c;
522 }));
524 // If it's the last one:
525 if (!it.hasNext()) {
526 // Add the concluding task, that waits on all and shuts down the scheduler
527 d3d.executors.submit(new Runnable() {
528 @Override
529 public void run() {
530 // Wait until all are done
531 for (final Future<Content> c : list) {
532 try {
533 c.get();
534 } catch (final Throwable t) {
535 IJError.print(t);
538 try {
539 // Shutdown scheduler and execute remaining tasks
540 for (final Runnable r : updater.shutdownNow()) {
541 r.run();
543 } catch (final Throwable e) {
544 IJError.print(e);
546 // Reset cursor
547 doneWaiting();
548 Utils.showStatus(new StringBuilder("Done rendering ").append(counter.get()).append('/').append(hs.size()).toString());
554 return list;
556 }});
558 if (wait && -1 != resample) {
559 try {
560 fu.get();
561 } catch (final Throwable t) {
562 IJError.print(t);
566 return fu;
569 static public void resetView(final LayerSet ls) {
570 final Display3D d3d = ht_layer_sets.get(ls);
571 if (null != d3d) d3d.universe.resetView();
574 static public void showOrthoslices(final Patch p) {
575 final Display3D d3d = get(p.getLayerSet());
576 d3d.adjustResampling();
577 //d3d.universe.resetView();
578 final String title = makeTitle(p) + " orthoslices";
579 // remove if present
580 d3d.universe.removeContent(title);
581 final PatchStack ps = p.makePatchStack();
582 final ImagePlus imp = get8BitStack(ps);
583 final Content ct = d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
584 setTransform(ct, ps.getPatch(0));
585 ct.setLocked(true); // locks the added content
588 static public void showVolume(final Patch p) {
589 final Display3D d3d = get(p.getLayerSet());
590 d3d.adjustResampling();
591 //d3d.universe.resetView();
592 final String title = makeTitle(p) + " volume";
593 // remove if present
594 d3d.universe.removeContent(title);
595 final PatchStack ps = p.makePatchStack();
596 final ImagePlus imp = get8BitStack(ps);
597 final Content ct = d3d.universe.addVoltex(imp, null, title, 0, new boolean[]{true, true, true}, d3d.resample);
598 setTransform(ct, ps.getPatch(0));
599 ct.setLocked(true); // locks the added content
602 static private void setTransform(final Content ct, final Patch p) {
603 final double[] a = new double[6];
604 p.getAffineTransform().getMatrix(a);
605 final Calibration cal = p.getLayerSet().getCalibration();
606 // a is: m00 m10 m01 m11 m02 m12
607 // d expects: m01 m02 m03 m04, m11 m12 ...
608 ct.applyTransform(new Transform3D(new double[]{a[0], a[2], 0, a[4] * cal.pixelWidth,
609 a[1], a[3], 0, a[5] * cal.pixelWidth,
610 0, 0, 1, p.getLayer().getZ() * cal.pixelWidth,
611 0, 0, 0, 1}));
614 static public void showOrthoslices(final ImagePlus imp, final String title, final int wx, final int wy, final float scale2D, final Layer first) {
615 final Display3D d3d = get(first.getParent());
616 d3d.universe.removeContent(title);
617 final Content ct = d3d.universe.addOrthoslice(imp, null, title, 0, new boolean[]{true, true, true}, 1);
618 final Calibration cal = imp.getCalibration();
619 final Transform3D t = new Transform3D(new double[]{1, 0, 0, wx * cal.pixelWidth * scale2D,
620 0, 1, 0, wy * cal.pixelHeight * scale2D,
621 0, 0, scale2D, first.getZ() * cal.pixelWidth * scale2D, // not pixelDepth!
622 0, 0, 0, 1});
623 // why scale2D has to be there at all reflects a horrible underlying setting of the calibration, plus of the scaling in the Display3D.
624 Utils.log(t);
625 ct.applyTransform(t);
626 ct.setLocked(true);
629 /** Returns a stack suitable for the ImageJ 3D Viewer, either 8-bit gray or 8-bit color.
630 * If the PatchStack is already of the right type, it is returned,
631 * otherwise a copy is made in the proper type.
633 static private ImagePlus get8BitStack(final PatchStack ps) {
634 switch (ps.getType()) {
635 case ImagePlus.COLOR_RGB:
636 // convert stack to 8-bit color
637 return ps.createColor256Copy();
638 case ImagePlus.GRAY16:
639 case ImagePlus.GRAY32:
640 // convert stack to 8-bit
641 return ps.createGray8Copy();
642 case ImagePlus.GRAY8:
643 case ImagePlus.COLOR_256:
644 return ps;
645 default:
646 Utils.logAll("Cannot handle stacks of type: " + ps.getType());
647 return null;
651 /** Considers there is only one Display3D for each LayerSet. */
652 static public void remove(final ProjectThing pt) {
653 if (null == pt) return;
654 if (null == pt.getObject()) return;
655 final Object ob = pt.getObject();
656 if (!(ob instanceof Displayable)) return;
657 final Displayable displ = (Displayable)ob;
658 final Display3D d3d = ht_layer_sets.get(displ.getLayerSet()); // TODO profile_list is going to fail here
659 if (null == d3d) {
660 // there is no Display3D showing the pt to remove
661 //Utils.log2("No Display3D contains ProjectThing: " + pt);
662 return;
664 String name;
665 synchronized (d3d.ht_pt_meshes) {
666 name = d3d.ht_pt_meshes.remove(pt);
668 if (null == name) {
669 Utils.log2("No mesh contained within " + d3d + " for ProjectThing " + pt);
670 return;
672 d3d.universe.removeContent(name);
676 /** Creates a mesh for the given Displayable in a separate Thread, and adds it to the universe. */
677 private Future<Content> addMesh(final ProjectThing pt, final Displayable displ, final int resample) {
678 return executors.submit(new Callable<Content>() {
679 @Override
680 public Content call() {
681 try {
682 // 1 - Create content
683 final Callable<Content> c = createMesh(pt, displ, resample);
684 if (null == c) return null;
685 final Content content = c.call();
686 if (null == content) return null;
687 final String title = content.getName();
688 // 2 - Remove from universe any content of the same title
689 if (universe.contains(title)) {
690 universe.removeContent(title);
692 // 3 - Add to universe, and wait
693 universe.addContentLater(content).get();
695 return content;
697 } catch (final Exception e) {
698 IJError.print(e);
699 return null;
705 static private final String makeProfileListTitle(final ProjectThing pt) {
706 String title;
707 final Object ob = pt.getParent().getTitle();
708 if (null == ob || ob.equals(pt.getParent().getType())) title = pt.toString() + " #" + pt.getId(); // Project.getMeaningfulTitle can't handle profile_list properly
709 else title = ob.toString() + " /[" + pt.getParent().getType() + "]/[profile_list] #" + pt.getId();
710 return title;
713 /** Remove all basic type children contained in {@param pt} and its children, recursively.
715 * @param pt
717 static public void removeFrom3D(final ProjectThing pt) {
718 final HashSet<ProjectThing> hs = pt.findBasicTypeChildren();
719 if (null == hs || 0 == hs.size()) {
720 Utils.logAll("Nothing to remove from 3D.");
721 return;
723 // Ignore Profile instances ("profile_list" takes care of them)
724 for (final ProjectThing child : hs) {
725 if (child.getByType().equals("profile")) continue;
726 // Find the LayerSet
727 LayerSet lset = null;
728 if (child.getType().equals("profile_list")) {
729 if (!child.hasChildren()) continue;
730 for (final ProjectThing p : child.getChildren()) {
731 if (null != p.getObject() && p.getObject() instanceof Profile) {
732 lset = ((Displayable)p.getObject()).getLayerSet();
733 break;
736 if (null == lset) continue;
737 } else if (child.getType().equals("profile")) {
738 // Taken care of by "profile list"
739 continue;
740 } else {
741 final Displayable d = (Displayable)child.getObject();
742 if (null == d) {
743 Utils.log("Null object for ProjectThing " + child);
744 continue;
746 lset = d.getLayerSet();
748 if (null == lset) {
749 Utils.log("No LayerSet found for " + child);
750 continue;
752 final Display3D d3d = getDisplay(lset);
753 if (null == d3d) {
754 Utils.log("No Display 3D found for " + child);
755 continue; // no Display3D window open
757 final String oldTitle = d3d.ht_pt_meshes.remove(child);
758 if (null == oldTitle) {
759 Utils.log("Could not find a title for " + child);
760 continue;
762 Utils.log("Removed from 3D view: " + oldTitle);
763 d3d.getUniverse().removeContent(oldTitle);
768 /** Returns a function that returns a Content object.
769 * Does NOT add the Content to the universe; it merely creates it. */
770 public Callable<Content> createMesh(final ProjectThing pt, final Displayable displ, final int resample) {
771 final double scale = 1.0; // OBSOLETE
772 return new Callable<Content>() {
773 @Override
774 public Content call() {
775 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
776 try {
778 // the list 'triangles' is really a list of Point3f, which define a triangle every 3 consecutive points. (TODO most likely Bene Schmid got it wrong: I don't think there's any need to have the points duplicated if they overlap in space but belong to separate triangles.)
779 final List<Point3f> triangles;
780 //boolean no_culling_ = false;
782 final Class<?> c;
783 final boolean line_mesh;
784 final int line_mesh_mode;
785 if (null == displ) {
786 c = null;
787 line_mesh = false;
788 line_mesh_mode = Integer.MAX_VALUE;
789 } else {
790 c = displ.getClass();
791 line_mesh = Tree.class.isAssignableFrom(c) || Polyline.class == c;
792 if (Tree.class.isAssignableFrom(c)) line_mesh_mode = CustomLineMesh.PAIRWISE;
793 else if (Polyline.class == c) line_mesh_mode = CustomLineMesh.CONTINUOUS;
794 else line_mesh_mode = Integer.MAX_VALUE; // disabled
797 List<Point3f> extra_triangles = null;
798 List<Color3f> triangle_colors = null,
799 extra_triangle_colors = null;
801 int rs = resample;
802 if (displ instanceof AreaContainer) {
803 if (-1 == resample) rs = Display3D.this.resample = adjustResampling(); // will adjust this.resample, and return it (even if it's a default value)
804 else rs = Display3D.this.resample;
806 if (AreaList.class == c) {
807 triangles = ((AreaList)displ).generateTriangles(scale, rs);
808 //triangles = removeNonManifold(triangles);
809 } else if (Ball.class == c) {
810 final double[][][] globe = Ball.generateGlobe(12, 12);
811 triangles = ((Ball)displ).generateTriangles(scale, globe);
812 } else if (displ instanceof Line3D) {
813 // Pipe and Polyline
814 // adjustResampling(); // fails horribly, needs first to correct mesh-generation code
815 triangles = ((Line3D)displ).generateTriangles(scale, 12, 1 /*Display3D.this.resample*/);
816 } else if (displ instanceof Tree<?>) {
817 // A 3D wire skeleton, using CustomLineMesh
818 final Tree.MeshData skeleton = ((Tree<?>)displ).generateSkeleton(scale, 12, 1);
819 triangles = skeleton.verts;
820 triangle_colors = skeleton.colors;
821 if (displ instanceof Treeline) {
822 final Tree.MeshData tube = ((Treeline)displ).generateMesh(scale, 12);
823 extra_triangles = tube.verts;
824 extra_triangle_colors = tube.colors;
825 } else if (displ instanceof AreaTree) {
826 final Tree.MeshData mesh = ((AreaTree)displ).generateMesh(scale, rs);
827 extra_triangles = mesh.verts;
828 extra_triangle_colors = mesh.colors;
830 if (null != extra_triangles && extra_triangles.isEmpty()) extra_triangles = null; // avoid issues with MultiMesh
831 } else if (Connector.class == c) {
832 final Tree.MeshData octopus = ((Connector)displ).generateMesh(scale, 12);
833 triangles = octopus.verts;
834 triangle_colors = octopus.colors;
835 } else if (null == displ && pt.getType().equals("profile_list")) {
836 triangles = Profile.generateTriangles(pt, scale);
837 //no_culling_ = true;
838 } else {
839 Utils.log("Unrecognized type for 3D mesh generation: " + (null != displ ? displ.getClass() : null) + " : " + displ);
840 triangles = null;
842 // safety checks
843 if (null == triangles) {
844 Utils.log("Some error ocurred: can't create triangles for " + displ);
845 return null;
847 if (0 == triangles.size()) {
848 Utils.log2("Skipping empty mesh for " + displ.getTitle());
849 return null;
851 if (!line_mesh && 0 != triangles.size() % 3) {
852 Utils.log2("Skipping non-multiple-of-3 vertices list generated for " + displ.getTitle());
853 return null;
856 final Color color;
857 final float alpha;
858 final String title;
859 if (null != displ) {
860 color = displ.getColor();
861 alpha = displ.getAlpha();
862 title = makeTitle(displ);
863 } else if (pt.getType().equals("profile_list")) {
864 // for profile_list: get from the first (what a kludge; there should be a ZDisplayable ProfileList object)
865 final Object obp = ((ProjectThing)pt.getChildren().get(0)).getObject();
866 if (null == obp) return null;
867 final Displayable di = (Displayable)obp;
868 color = di.getColor();
869 alpha = di.getAlpha();
870 title = makeProfileListTitle(pt);
871 } else {
872 title = pt.toString() + " #" + pt.getId();
873 color = null;
874 alpha = 1.0f;
877 // Why for all? Above no_culling_ is set to true or false, depending upon type. --> Because with transparencies it looks proper and better when no_culling is true.
878 final boolean no_culling = true; // for ALL
880 Content ct = null;
882 try {
883 final Color3f c3 = new Color3f(color);
885 // If it exists, remove and add as new:
886 universe.removeContent(title);
888 final CustomMesh cm;
890 if (line_mesh) {
891 //ct = universe.createContent(new CustomLineMesh(triangles, line_mesh_mode, c3, 0), title);
892 cm = new CustomLineMesh(triangles, line_mesh_mode, c3, 0);
893 } else if (no_culling) {
894 // create a mesh with the same color and zero transparency (that is, full opacity)
895 final CustomTriangleMesh mesh = new CustomTriangleMesh(triangles, c3, 0);
896 // Set mesh properties for double-sided triangles
897 final PolygonAttributes pa = mesh.getAppearance().getPolygonAttributes();
898 pa.setCullFace(PolygonAttributes.CULL_NONE);
899 pa.setBackFaceNormalFlip(true);
900 mesh.setColor(c3);
901 // After setting properties, add to the viewer
902 //ct = universe.createContent(mesh, title);
903 cm = mesh;
904 } else {
905 //ct = universe.createContent(new CustomTriangleMesh(triangles, c3, 0), title);
906 cm = new CustomTriangleMesh(triangles, c3, 0);
909 if (null != triangle_colors) cm.setColor(triangle_colors);
911 //if (null == cm) return null;
913 if (null == extra_triangles || 0 == extra_triangles.size()) {
914 ct = universe.createContent(cm, title);
915 } else {
916 final CustomTriangleMesh extra = new CustomTriangleMesh(extra_triangles, c3, 0);
917 if (null != extra_triangle_colors) {
918 // Set mesh properties for double-sided triangles
919 final PolygonAttributes pa = extra.getAppearance().getPolygonAttributes();
920 pa.setCullFace(PolygonAttributes.CULL_NONE);
921 pa.setBackFaceNormalFlip(true);
922 extra.setColor(extra_triangle_colors);
924 ct = universe.createContent(new CustomMultiMesh(Arrays.asList(new CustomMesh[]{cm, extra})), title);
927 // Set general content properties
928 ct.setTransparency(1f - alpha);
929 // Default is unlocked (editable) transformation; set it to locked:
930 ct.setLocked(true);
932 // register mesh title
933 synchronized (ht_pt_meshes) {
934 ht_pt_meshes.put(pt, ct.getName());
937 } catch (final Throwable e) {
938 Utils.logAll("Mesh generation failed for \"" + title + "\" from " + pt);
939 IJError.print(e);
940 e.printStackTrace();
943 Utils.log2(pt.toString() + " n points: " + triangles.size());
945 return ct;
947 } catch (final Exception e) {
948 IJError.print(e);
949 return null;
955 static public class VectorStringContent {
956 VectorString3D vs;
957 String title;
958 Color color;
959 double[] widths;
960 float alpha;
961 public VectorStringContent(final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha){
962 this.vs = vs;
963 this.title = title;
964 this.color = color;
965 this.widths = widths;
966 this.alpha = alpha;
968 public Content asContent(final Display3D d3d) {
969 double[] wi = widths;
970 if (null == widths) {
971 wi = new double[vs.getPoints(0).length];
972 Arrays.fill(wi, 2.0);
973 } else if (widths.length != vs.length()) {
974 Utils.log("ERROR: widths.length != VectorString3D.length()");
975 return null;
977 float transp = 1 - alpha;
978 if (transp < 0) transp = 0;
979 if (transp > 1) transp = 1;
980 if (1 == transp) {
981 Utils.log("WARNING: adding a 3D object fully transparent.");
983 final List<Point3f> triangles = Pipe.generateTriangles(Pipe.makeTube(vs.getPoints(0), vs.getPoints(1), vs.getPoints(2), wi, 1, 12, null), d3d.scale);
984 final Content ct = d3d.universe.createContent(new CustomTriangleMesh(triangles, new Color3f(color), 0), title);
985 ct.setTransparency(transp);
986 ct.setLocked(true);
987 return ct;
991 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
992 static public Future<Collection<Future<Content>>> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color) {
993 return addMesh(ref_ls, vs, title, color, null, 1.0f);
996 /** Creates a mesh from the given VectorString3D, which is unbound to any existing Pipe. */
997 static public Future<Collection<Future<Content>>> addMesh(final LayerSet ref_ls, final VectorString3D vs, final String title, final Color color, final double[] widths, final float alpha) {
998 final List<Content> col = new ArrayList<Content>();
999 final Display3D d3d = Display3D.get(ref_ls);
1000 col.add(new VectorStringContent(vs, title, color, widths, alpha).asContent(d3d));
1001 return d3d.addContent(col);
1004 static public Future<Collection<Future<Content>>> show(final LayerSet ref_ls, final Collection<Content> col) {
1005 final Display3D d3d = get(ref_ls);
1006 return d3d.addContent(col);
1009 public Future<Collection<Future<Content>>> addContent(final Collection<Content> col) {
1011 final FutureTask<Collection<Future<Content>>> fu = new FutureTask<Collection<Future<Content>>>(new Callable<Collection<Future<Content>>>() {
1012 @Override
1013 public Collection<Future<Content>> call() {
1014 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
1015 try {
1016 return universe.addContentLater(col);
1017 } catch (final Throwable e) {
1018 IJError.print(e);
1019 return null;
1021 }});
1023 launchers.submit(new Runnable() { @Override
1024 public void run() {
1025 executors.submit(fu);
1026 }});
1028 return fu;
1031 public Future<Content> addContent(final Content c) {
1032 final FutureTask<Content> fu = new FutureTask<Content>(new Callable<Content>() {
1033 @Override
1034 public Content call() {
1035 Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
1036 try {
1037 return universe.addContentLater(c).get();
1038 } catch (final Throwable e) {
1039 IJError.print(e);
1040 return null;
1045 launchers.submit(new Runnable() { @Override
1046 public void run() {
1047 executors.submit(fu);
1048 }});
1050 return fu;
1053 static public final int estimateResamplingFactor(final LayerSet ls, final double width, final double height) {
1054 final int max_dimension = ls.getPixelsMaxDimension();
1055 return (int)(DEFAULT_RESAMPLE / (Math.max(width, height) > max_dimension ?
1056 max_dimension / Math.max(width, height)
1057 : 1));
1060 /** Estimate a scaling factor, to be used as a multiplier of the suggested default resampling. */
1061 private final int estimateResamplingFactor() {
1062 return estimateResamplingFactor(layer_set, width, height);
1065 // This method has the exclusivity in adjusting the resampling value, and it also returns it.
1066 synchronized private final int adjustResampling() {
1067 if (resample > 0) return resample;
1068 final GenericDialog gd = new GenericDialog("Resample");
1069 final int default_resample = estimateResamplingFactor();
1070 gd.addSlider("Resample: ", 1, Math.max(default_resample, 100), -1 != resample ? resample : default_resample);
1071 gd.showDialog();
1072 if (gd.wasCanceled()) {
1073 resample = -1 != resample ? resample : default_resample; // current or default value
1074 return resample;
1076 resample = ((java.awt.Scrollbar)gd.getSliders().get(0)).getValue();
1077 return resample;
1080 /** Checks if there is any Display3D instance currently showing the given Displayable. */
1081 static public boolean isDisplayed(final Displayable d) {
1082 if (null == d) return false;
1083 final String title = makeTitle(d);
1084 for (final Display3D d3d : ht_layer_sets.values()) {
1085 if (null != d3d.universe.getContent(title)) return true;
1087 if (d.getClass() == Profile.class) {
1088 if (null != getProfileContent(d)) return true;
1090 return false;
1093 /** Checks if the given Displayable is a Profile, and tries to find a possible Content object in the Image3DUniverse of its LayerSet according to the title as created from its profile_list ProjectThing. */
1094 static public Content getProfileContent(final Displayable d) {
1095 if (null == d) return null;
1096 if (d.getClass() != Profile.class) return null;
1097 final Display3D d3d = get(d.getLayer().getParent());
1098 if (null == d3d) return null;
1099 ProjectThing pt = d.getProject().findProjectThing(d);
1100 if (null == pt) return null;
1101 pt = (ProjectThing) pt.getParent();
1102 return d3d.universe.getContent(new StringBuilder(pt.toString()).append(" #").append(pt.getId()).toString());
1105 static public Future<Boolean> setColor(final Displayable d, final Color color) {
1106 final Display3D d3d = getDisplay(d.getLayer().getParent());
1107 if (null == d3d) return null; // no 3D displays open
1108 return d3d.executors.submit(new Callable<Boolean>() { @Override
1109 public Boolean call() {
1110 Content content = d3d.universe.getContent(makeTitle(d));
1111 if (null == content) content = getProfileContent(d);
1112 if (null != content) {
1113 content.setColor(new Color3f(color));
1114 return true;
1116 return false;
1117 }});
1120 static public Future<Boolean> setTransparency(final Displayable d, final float alpha) {
1121 if (null == d) return null;
1122 final Layer layer = d.getLayer();
1123 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
1124 final Display3D d3d = ht_layer_sets.get(layer.getParent());
1125 if (null == d3d) return null;
1126 return d3d.executors.submit(new Callable<Boolean>() { @Override
1127 public Boolean call() {
1128 String title = makeTitle(d);
1129 Content content = d3d.universe.getContent(title);
1130 if (null == content) content = getProfileContent(d);
1131 if (null != content) content.setTransparency(1 - alpha);
1132 else if (null == content && d.getClass().equals(Patch.class)) {
1133 final Patch pa = (Patch)d;
1134 if (pa.isStack()) {
1135 title = pa.getProject().getLoader().getFileName(pa);
1136 for (final Display3D dd : ht_layer_sets.values()) {
1137 for (final Iterator<?> cit = dd.universe.getContents().iterator(); cit.hasNext(); ) {
1138 final Content c = (Content)cit.next();
1139 if (c.getName().startsWith(title)) {
1140 c.setTransparency(1 - alpha);
1141 // no break, since there could be a volume and an orthoslice
1147 return true;
1148 }});
1151 static public String makeTitle(final Displayable d) {
1152 return d.getProject().getMeaningfulTitle(d) + " #" + d.getId();
1154 static public String makeTitle(final Patch p) {
1155 return new File(p.getProject().getLoader().getAbsolutePath(p)).getName()
1156 + " #" + p.getProject().getLoader().getNextId();
1159 /** Remake the mesh for the Displayable in a separate Thread, if it's included in a Display3D
1160 * (otherwise returns null). */
1161 static public Future<Content> update(final Displayable d) {
1162 final Layer layer = d.getLayer();
1163 if (null == layer) return null; // some objects have no layer, such as the parent LayerSet.
1164 final Display3D d3d = ht_layer_sets.get(layer.getParent());
1165 if (null == d3d) return null;
1166 return d3d.addMesh(d.getProject().findProjectThing(d), d, d3d.resample);
1170 static public final double computeTriangleArea() {
1171 return 0.5 * Math.sqrt(Math.pow(xA*yB + xB*yC + xC*yA, 2) +
1172 Math.pow(yA*zB + yB*zC + yC*zA, 2) +
1173 Math.pow(zA*xB + zB*xC + zC*xA, 2));
1177 static public final boolean contains(final LayerSet ls, final String title) {
1178 final Display3D d3d = getDisplay(ls);
1179 if (null == d3d) return false;
1180 return null != d3d.universe.getContent(title);
1183 static public void destroy() {
1184 launchers.shutdownNow();
1187 static public void init() {
1188 if (launchers.isShutdown()) {
1189 launchers = Utils.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), "Display3D-launchers");
1193 /** Creates a calibrated sphere to represent a point at LayerSet pixel coordinates wx, wy, wz, with radius wr.*/
1194 public List<Point3f> createFatPoint(final double wx, final double wy, final double wz, final double wr, final Calibration cal) {
1195 final double[][][] globe = Ball.generateGlobe(12, 12);
1196 final int sign = cal.pixelDepth < 0 ? -1 : 1;
1197 for (int z=0; z<globe.length; z++) {
1198 for (int k=0; k<globe[0].length; k++) {
1199 globe[z][k][0] = (globe[z][k][0] * wr + wx) * scale * cal.pixelWidth;
1200 globe[z][k][1] = (globe[z][k][1] * wr + wy) * scale * cal.pixelHeight;
1201 globe[z][k][2] = (globe[z][k][2] * wr + wz) * scale * cal.pixelWidth * sign; // not pixelDepth, see day notes 20080227. Because pixelDepth is in microns/px, not in px/microns, and the z coord here is taken from the z of the layer, which is in pixels.
1204 final ArrayList<Point3f> list = new ArrayList<Point3f>();
1205 // create triangular faces and add them to the list
1206 for (int z=0; z<globe.length-1; z++) { // the parallels
1207 for (int k=0; k<globe[0].length -1; k++) { // meridian points
1208 // half quadrant (a triangle)
1209 list.add(new Point3f((float)globe[z][k][0], (float)globe[z][k][1], (float)globe[z][k][2]));
1210 list.add(new Point3f((float)globe[z+1][k+1][0], (float)globe[z+1][k+1][1], (float)globe[z+1][k+1][2]));
1211 list.add(new Point3f((float)globe[z+1][k][0], (float)globe[z+1][k][1], (float)globe[z+1][k][2]));
1212 // the other half quadrant
1213 list.add(new Point3f((float)globe[z][k][0], (float)globe[z][k][1], (float)globe[z][k][2]));
1214 list.add(new Point3f((float)globe[z][k+1][0], (float)globe[z][k+1][1], (float)globe[z][k+1][2]));
1215 list.add(new Point3f((float)globe[z+1][k+1][0], (float)globe[z+1][k+1][1], (float)globe[z+1][k+1][2]));
1218 return list;
1221 /** Expects uncalibrated wx,wy,wz, (i.e. pixel values), to be calibrated by @param ls calibration. */
1222 static public final Future<Content> addFatPoint(final String title, final LayerSet ls, final double wx, final double wy, final double wz, final double wr, final Color color) {
1223 final Display3D d3d = Display3D.get(ls);
1224 d3d.universe.removeContent(title);
1225 final Content ct = d3d.universe.createContent(new CustomTriangleMesh(d3d.createFatPoint(wx, wy, wz, wr, ls.getCalibrationCopy()), new Color3f(color), 0), title);
1226 ct.setLocked(true);
1227 return d3d.addContent(ct);