001 /*
002 Copyright (c) 1996-2012, Damon Hart-Davis
003 All rights reserved.
004
005 Redistribution and use in source and binary forms, with or without
006 modification, are permitted provided that the following conditions are
007 met:
008
009 * Redistributions of source code must retain the above copyright
010 notice, this list of conditions and the following disclaimer.
011
012 * Redistributions in binary form must reproduce the above copyright
013 notice, this list of conditions and the following disclaimer in the
014 documentation and/or other materials provided with the
015 distribution.
016
017 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028 */
029
030 package org.hd.d.pg2k.webSvr.threeD;
031
032 import java.awt.BorderLayout;
033 import java.awt.Dimension;
034 import java.awt.Font;
035 import java.awt.GraphicsConfiguration;
036 import java.awt.event.ActionEvent;
037 import java.awt.event.ActionListener;
038 import java.awt.event.MouseEvent;
039 import java.awt.event.MouseListener;
040 import java.awt.event.WindowAdapter;
041 import java.awt.event.WindowEvent;
042 import java.net.URL;
043 import java.util.Arrays;
044 import java.util.Enumeration;
045 import java.util.concurrent.atomic.AtomicInteger;
046
047 import javax.jnlp.SingleInstanceListener;
048 import javax.media.j3d.Alpha;
049 import javax.media.j3d.Appearance;
050 import javax.media.j3d.Behavior;
051 import javax.media.j3d.BoundingSphere;
052 import javax.media.j3d.Bounds;
053 import javax.media.j3d.BranchGroup;
054 import javax.media.j3d.Canvas3D;
055 import javax.media.j3d.ColoringAttributes;
056 import javax.media.j3d.Fog;
057 import javax.media.j3d.Font3D;
058 import javax.media.j3d.FontExtrusion;
059 import javax.media.j3d.Group;
060 import javax.media.j3d.LinearFog;
061 import javax.media.j3d.Locale;
062 import javax.media.j3d.Node;
063 import javax.media.j3d.PickInfo;
064 import javax.media.j3d.RotationInterpolator;
065 import javax.media.j3d.Shape3D;
066 import javax.media.j3d.TexCoordGeneration;
067 import javax.media.j3d.Text3D;
068 import javax.media.j3d.Texture;
069 import javax.media.j3d.Transform3D;
070 import javax.media.j3d.TransformGroup;
071 import javax.media.j3d.View;
072 import javax.media.j3d.ViewPlatform;
073 import javax.media.j3d.VirtualUniverse;
074 import javax.media.j3d.WakeupCriterion;
075 import javax.media.j3d.WakeupOnElapsedTime;
076 import javax.media.j3d.WakeupOnViewPlatformEntry;
077 import javax.media.j3d.WakeupOnViewPlatformExit;
078 import javax.media.j3d.WakeupOr;
079 import javax.swing.BorderFactory;
080 import javax.swing.JComponent;
081 import javax.swing.JFrame;
082 import javax.swing.JLabel;
083 import javax.swing.JOptionPane;
084 import javax.swing.JPanel;
085 import javax.swing.JSlider;
086 import javax.swing.SwingConstants;
087 import javax.swing.SwingUtilities;
088 import javax.swing.Timer;
089 import javax.swing.UIManager;
090 import javax.vecmath.Color3f;
091 import javax.vecmath.Point3d;
092 import javax.vecmath.Point3f;
093 import javax.vecmath.Vector3d;
094 import javax.vecmath.Vector3f;
095 import javax.vecmath.Vector4f;
096
097 import org.hd.d.pg2k.svrCore.AbstractSimpleLogger;
098 import org.hd.d.pg2k.svrCore.CoreConsts;
099 import org.hd.d.pg2k.svrCore.ExhibitName;
100 import org.hd.d.pg2k.svrCore.MemoryTools;
101 import org.hd.d.pg2k.svrCore.Name;
102 import org.hd.d.pg2k.svrCore.Rnd;
103 import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
104 import org.hd.d.pg2k.webSvr.util.WebConsts;
105
106 import ORG.hd.d.IsDebug;
107
108 import com.sun.j3d.utils.behaviors.mouse.MouseTranslate;
109 import com.sun.j3d.utils.behaviors.mouse.MouseZoom;
110 import com.sun.j3d.utils.geometry.Primitive;
111 import com.sun.j3d.utils.geometry.Sphere;
112 import com.sun.j3d.utils.geometry.Text2D;
113 import com.sun.j3d.utils.pickfast.PickCanvas;
114 import com.sun.j3d.utils.universe.SimpleUniverse;
115 import com.sun.j3d.utils.universe.ViewingPlatform;
116
117
118 /**Main (UI) class of JWS-based 3D walkthrough.
119 * Runs as a Swing App.
120 * <p>
121 * This class/file contains as little non-UI code as is reasonably practical,
122 * so that if we change the UI details then other classes should be unaffected.
123 * <p>
124 * We don't really want/need this class to be Serializable,
125 * but this class inherits Serializable from JFrame.
126 */
127 public final class ThreeDMain extends JFrame // implements ActionListener
128 {
129 /**Central logger instance for uploader; never null.
130 * This instance may log to the status bar and elsewhere.
131 */
132 private final SimpleLoggerIF logger = new AbstractSimpleLogger()
133 {
134 public final void log(final String message)
135 {
136 // Log to the Java console.
137 System.out.println(message);
138
139 // Schedule a job for the event-dispatching thread:
140 // logging this message in the status bar.
141 // We hope that these events do not get re-ordered too often.
142 SwingUtilities.invokeLater(new Runnable(){
143 public final void run()
144 { status.setText(message); }
145 });
146 }
147 };
148
149 /**Our companion "business-logic" class; never null. */
150 private final ThreeDLogic logic = new ThreeDLogic(logger);
151
152 // /**The action performed by the "About" menu entry. */
153 // private final AboutAction aboutAction = new AboutAction();
154 // /**The action performed by the "Exit" menu entry. */
155 // private final ExitAction exitAction = new ExitAction();
156
157 // /**Select our locale; never null. */
158 // private final LocaleBeanBase lbb = new LocaleBeanBase();
159
160 /**Status bar; never null. */
161 private final JLabel status = createStatusBar();
162
163 /**Single listener instance. */
164 private final SISListener sisListener = new SISListener();
165
166 // /**Handles Mouse over messages on toolbar buttons and menu items; never null. */
167 // private final MouseHandler mouseHandler = new MouseHandler(status);
168
169 /**Title shown for application. */
170 private static final String APPLICATION_WINDOW_TITLE = "Gallery 3D Walkthrough";
171
172 /**Our simple universe; only null if Java3D not available or set-up not yet complete.
173 * Marked volatile for thread-safe lock-free access.
174 */
175 private volatile SimpleUniverse simpleUniverse;
176
177 /**3D canvas for Java3D to draw on; only null if Java3D not available. */
178 private final Canvas3D canvas3D;
179
180 /**BranchGroup used to hold the exhibits; never null.
181 * This is set up to be writable/extendable at runtime
182 * so that we can update the exhibits scene as needed.
183 */
184 private final BranchGroup exhibitsBranchGroup = new BranchGroup();
185
186 /**Allow exhibit children to be added/removed at run-time. */
187 {
188 exhibitsBranchGroup.setCapability(Group.ALLOW_CHILDREN_READ);
189 exhibitsBranchGroup.setCapability(Group.ALLOW_CHILDREN_WRITE);
190 exhibitsBranchGroup.setCapability(Group.ALLOW_CHILDREN_EXTEND);
191 }
192
193 /**X-axis slider; never null. */
194 private final JSlider sliderX = new JSlider(SwingConstants.HORIZONTAL);
195
196 /**Y-axis slider; never null. */
197 private final JSlider sliderY = new JSlider(SwingConstants.VERTICAL);
198
199 /**Z-axis slider; never null. */
200 private final JSlider sliderZ = new JSlider(SwingConstants.VERTICAL);
201
202
203 /**Create an instance of the Uploader app main window.
204 * Designed to be called by main().
205 */
206 private ThreeDMain()
207 {
208 super(APPLICATION_WINDOW_TITLE);
209
210 // // Set up user actions.
211 // initActions();
212
213 // ASAP, try to avoid multiple instances being started.
214 if(logic.sis != null)
215 { logic.sis.addSingleInstanceListener(sisListener); }
216
217 // Try to create 3D canvas.
218 Canvas3D c3D = null;
219 try
220 {
221 final GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
222 c3D = new Canvas3D(config);
223 }
224 catch(final Throwable t)
225 {
226 logger.log("Unable to create Canvas3D...");
227 t.printStackTrace();
228 }
229 canvas3D = c3D;
230
231 // Create the contents of this frame...
232 // setJMenuBar(createMenu());
233 final JComponent mainPane = create3DPane();
234 getContentPane().add(mainPane, BorderLayout.CENTER);
235 getContentPane().add(status, BorderLayout.SOUTH);
236
237 // Arrange to exit the VM if the window is closed...
238 addWindowListener(new WindowAdapter()
239 {
240 @Override
241 public final void windowClosing(final WindowEvent evt)
242 {
243 // Shut down gracefully (and exit()) if allowed to,
244 // else attempt to veto with an exception.
245 try { shutdown(); }
246 catch(final UnsupportedOperationException e) { }
247 }
248 });
249 }
250
251 /**Perform any activity required to shut down cleanly, eg save state, then exit.
252 * This should try to avoid taking a long time.
253 * <p>
254 * This may throw an UnsupportedOperationException to try to veto
255 * an exit that the user changes their mind about,
256 * eg because we have work in progress!
257 *
258 * @throws UnsupportedOperationException if the user vetoes the shut-down
259 */
260 private void shutdown()
261 throws UnsupportedOperationException
262 {
263 // Do any shutdown required by the non-GUI components...
264 logic.shutdown();
265
266 // Now allow other instances to be started...
267 if(logic.sis != null)
268 { logic.sis.removeSingleInstanceListener(sisListener); }
269
270 // Now perform a gracefull exit!
271 System.exit(0);
272 }
273
274 // /**This method should be called before creating the UI to create all the Actions. */
275 // private void initActions()
276 // {
277 // registerAction(aboutAction);
278 // registerAction(exitAction);
279 // }
280 //
281 // private void registerAction(final JLFAbstractAction action)
282 // {
283 // action.addActionListener(this);
284 //// actions.addElement(action);
285 // }
286
287 // /**Create the application menu bar. */
288 // private JMenuBar createMenu()
289 // {
290 // final JMenuBar menuBar = new JMenuBar();
291 //
292 // // Build the File menu
293 // final JMenu fileMenu = new JMenu("File");
294 // fileMenu.setMnemonic('F');
295 // fileMenu.add(exitAction).addMouseListener(mouseHandler);
296 //
297 // // Build the help menu
298 // final JMenu helpMenu = new JMenu("Help");
299 // helpMenu.setMnemonic('H');
300 // helpMenu.add(aboutAction).addMouseListener(mouseHandler);
301 //
302 // menuBar.add(fileMenu);
303 // menuBar.add(helpMenu);
304 //
305 // return menuBar;
306 // }
307
308 /**Canvas width in pixels. */
309 private static final int CANVAS3D_WIDTH = 512;
310
311 /**Canvas height in pixels. */
312 private static final int CANVAS3D_HEIGHT = 512;
313
314 /**Create the (main) 3D component, called during construction; never null. */
315 private JPanel create3DPane()
316 {
317 final JPanel result = new JPanel();
318 result.setLayout(new BorderLayout());
319 if(canvas3D != null)
320 {
321 canvas3D.setSize(CANVAS3D_WIDTH, CANVAS3D_HEIGHT);
322 result.add(canvas3D, BorderLayout.CENTER);
323 result.add(sliderX, BorderLayout.SOUTH);
324 result.add(sliderY, BorderLayout.EAST);
325 result.add(sliderZ, BorderLayout.WEST);
326
327 // Some minimal decoration...
328 sliderX.setToolTipText("X-axis");
329 sliderY.setToolTipText("Y-axis");
330 sliderZ.setToolTipText("Z-axis");
331 }
332 else
333 {
334 result.add(new JLabel("Unable to start Java3D"), BorderLayout.CENTER);
335 result.setPreferredSize(new Dimension(CANVAS3D_WIDTH, CANVAS3D_HEIGHT));
336 logger.log("java.library.path=" + System.getProperty("java.library.path"));
337 }
338 return(result);
339 }
340 /**If true, animate the name banner (at the cost of CPU time, etc). */
341 private static final boolean ANIMATE_BANNER = false;
342
343 /**Maximum exhibit dimension (eg height or width) in metres; strictly positive.
344 * A value of around 1.0m, eg less than the viewer's nominal height, may be good.
345 */
346 public static final float MAX_EXHIBIT_DIM_M = 1.0f;
347
348 /**Exhibit (centres) spacing in metres; strictly positive and no smaller than MAX_EXHIBIT_DIM_M.
349 * A value of around twice MAX_EXHIBIT_DIM_M, may be good.
350 */
351 public static final float EXHIBIT_CSPACING_M = 3.0f * MAX_EXHIBIT_DIM_M;
352
353 /**Exhibit visibility spacing in metres; strictly positive and no smaller than MAX_EXHIBIT_DIM_M.
354 * When out of this radius we can save CPU and memory
355 * by reverting to a fall-back view.
356 * <p>
357 * We ensure that the nearest exhibit to the user
358 * is visible in their initial view.
359 */
360 public static final float EXHIBIT_VISIBLE_M = Math.max(15, // At least 15m view...
361 Math.max(3*MAX_EXHIBIT_DIM_M, 4.5f*EXHIBIT_CSPACING_M)); // See ~4 exhibits away.
362
363 /**Distance at which fog starts to have an effect, in open range ]0, EXHIBIT_VISIBLE_M[. */
364 private static final float FOG_START = Math.min(2.5f * EXHIBIT_CSPACING_M,
365 0.8f * EXHIBIT_VISIBLE_M);
366
367 // /**Distance at which objects are considered barely visible, in open range ]FOG_START, EXHIBIT_VISIBLE_M[. */
368 // private static final float BARELY_VISIBLE = (FOG_START + 3*EXHIBIT_VISIBLE_M) / 4;
369
370 /**Colour of text for captions, etc, logically immutable; never null. */
371 private static final Color3f TEXT_COLOUR = new Color3f(1,1,0);
372
373 /**Alternate/highlight/not-good colour of text/othe3r for captions, etc, logically immutable; never null. */
374 private static final Color3f TEXT_ALT_COLOUR = new Color3f(1,0.25f,0.25f);
375
376 /**Alternate/highlight/not-good colour of text/othe3r for captions, etc, logically immutable; never null. */
377 private static final Color3f TEXT_ALT2_COLOUR = new Color3f(1,0.5f,0.5f);
378
379 /**Alternate/highlight/not-good colour of text/othe3r for captions, etc, logically immutable; never null. */
380 private static final Color3f TEXT_ALT3_COLOUR = new Color3f(1,0.8f,0.8f);
381
382 /**Alternate/highlight/OK colour of text/other for captions, etc, logically immutable; never null. */
383 private static final Color3f TEXT_OK_COLOUR = new Color3f(0.25f,1,0.25f);
384
385 // /**Background colour of text for captions, etc, logically immutable; never null. */
386 // private static final Color3f TEXT_BG_COLOUR = new Color3f(0,0,0.25f);
387
388 /**Standard text colour/appearance, logically immutable; never null. */
389 private static final Appearance TEXT_APPEARANCE = new Appearance();
390 /**Initialise textAppearance. */
391 static
392 {
393 final ColoringAttributes textColouring = new ColoringAttributes(TEXT_COLOUR, ColoringAttributes.SHADE_FLAT);
394 TEXT_APPEARANCE.setColoringAttributes(textColouring);
395 }
396
397 // /**Standard text background colour/appearance, logically immutable; never null. */
398 // private static final Appearance TEXT_BG_APPEARANCE = new Appearance();
399 // /**Initialise textAppearance. */
400 // static
401 // {
402 // final ColoringAttributes textBgColouring = new ColoringAttributes(TEXT_BG_COLOUR, ColoringAttributes.SHADE_FLAT);
403 // TEXT_BG_APPEARANCE.setColoringAttributes(textBgColouring);
404 // }
405
406 /**Creates the PG2K banner branch graph; never null.
407 * This sits just above (0,0,0), is 3D text, is about human height,
408 * and may have some animation effects...
409 * <p/>
410 * Derived from the "HelloUniverse" example!
411 * <p>
412 * We add some other misc bits of the scene here, such as fog.
413 */
414 private static BranchGroup createMainBannerSceneGraph(final Point3f bottomRight)
415 {
416 // Create the root of the branch graph
417 final BranchGroup objRoot = new BranchGroup();
418
419 // Create the transform group node and initialize it to the
420 // identity. Enable the TRANSFORM_WRITE capability so that
421 // our behavior code can modify it at runtime. Add it to the
422 // root of the subgraph.
423 final TransformGroup objTrans = new TransformGroup();
424 if(ANIMATE_BANNER)
425 { objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); }
426 objRoot.addChild(objTrans);
427
428
429 // Create a text node...
430 final int textHeightMetres = 1;
431 final Text3D bannerText = new Text3D(new Font3D(new Font("Serif", Font.PLAIN, textHeightMetres), new FontExtrusion()),
432 CoreConsts.MAIN_DATA_HOST,
433 bottomRight,
434 Text3D.ALIGN_LAST,
435 Text3D.PATH_RIGHT);
436 final Appearance textAppearance = new Appearance();
437 final ColoringAttributes textColouring = new ColoringAttributes(1,0,0, ColoringAttributes.SHADE_GOURAUD);
438 textAppearance.setColoringAttributes(textColouring);
439 final Shape3D textShape = new Shape3D(bannerText, textAppearance);
440 objTrans.addChild(textShape);
441
442 if(ANIMATE_BANNER)
443 {
444 // Create a new Behavior object that will perform the desired
445 // operation on the specified transform object and add it into
446 // the scene graph.
447 final Transform3D yAxis = new Transform3D();
448 final Alpha rotationAlpha = new Alpha(-1, Alpha.INCREASING_ENABLE,
449 0, 0,
450 8000, 0, 0,
451 0, 0, 0);
452
453 final RotationInterpolator rotator =
454 new RotationInterpolator(rotationAlpha, objTrans, yAxis,
455 0.0f, (float) Math.PI * 2.0f)
456 /* {
457 @Override public final void processStimulus(final Enumeration enumeration)
458 {
459 super.processStimulus(enumeration);
460 if(IsDebug.isDebug) { System.err.println("Rotator got stimulus..."); }
461 }
462 } */ ;
463 // Don't waste CPU time animating banner when out of sight...
464 // Keep this to a minimal area to intersect with the view platform.
465 final BoundingSphere bounds =
466 new BoundingSphere(new Point3d(), EXHIBIT_VISIBLE_M);
467 rotator.setSchedulingBounds(bounds);
468 objTrans.addChild(rotator);
469 }
470
471
472 // Create influencing bounds for fog.
473 final Bounds fogBounds = new BoundingSphere(new Point3d(), Double.POSITIVE_INFINITY);
474 // Set the fog colour, density, and its influencing bounds
475 final Color3f FOG_COLOUR = new Color3f(); // Black, to match backdrop.
476 final Fog fog = new LinearFog(FOG_COLOUR, FOG_START, EXHIBIT_VISIBLE_M);
477 // front and back distances set below
478 // fog.setCapability(Fog.ALLOW_INFLUENCING_BOUNDS_WRITE);
479 fog.setInfluencingBounds(fogBounds);
480 objRoot.addChild(fog);
481
482
483 // Have Java 3D perform optimisations on this scene graph.
484 objRoot.compile();
485
486 return(objRoot);
487 }
488
489
490 /**Create one exhibit's "space" and "sphere of influence"; never null.
491 * This centres the exhibit on the point given,
492 * and surrounds it with concentric bounds:
493 * <ol>
494 * <li>An inner bound at which the exhibit can be seen/heard/etc in hi-fi.
495 * <li>A bound at which the exhibit can be seen, maybe at lo-fi,
496 * and may have the hi-fi version (pre-)fetched as needed.
497 * <li>An outer bound at which the exhibit is pre-fetched at lo-fi
498 * and possibly seen.
499 * </ol>
500 * <p>
501 * When outside the lo-red bounds the exhibit may be invisible
502 * and indeed only the scheduling bounds may exist to conserve memory.
503 *
504 * @param centre the centre of the exhibit and exhibit's space; never null
505 * @param index the index in the server's ordered list of exhibits;
506 * non-negative
507 */
508 private Node makeExhibitSpace(final Point3f centre,
509 final int index)
510 {
511 assert((centre != null) && (index >= 0));
512
513 final Point3d centre3d = new Point3d(centre);
514
515 // BranchGroup of reconstructable/discardable items
516 // that exist only when in view
517 // to save memory and CPU time.
518 final BranchGroup discardable = new BranchGroup();
519 discardable.setCapability(Group.ALLOW_CHILDREN_READ);
520 discardable.setCapability(Group.ALLOW_CHILDREN_WRITE);
521 discardable.setCapability(Group.ALLOW_CHILDREN_EXTEND);
522 discardable.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
523
524 final Behavior appearanceBehaviour = new Behavior()
525 {
526 /**Bounds within which exhibit thumbnail texture is displayed at all.
527 * Outside this area the exhibit cannot nominally be seen at all.
528 * <p>
529 * This is also the scheduling bounds for each exhibit.
530 */
531 private final BoundingSphere BOUNDS_LORES = new BoundingSphere(centre3d, EXHIBIT_VISIBLE_M);
532
533 /**Bounds within which exhibit thumbnail can be seen in high-res.
534 * This is mainly the unique space around the exhibit
535 * though there may be some overlap.
536 */
537 private final BoundingSphere BOUNDS_HIRES = new BoundingSphere(centre3d, EXHIBIT_CSPACING_M*0.8f);
538
539 /**Normal low-res visibility entry criterion. */
540 private final WakeupOnViewPlatformEntry loResWakeupOnViewPlatformEntry = new WakeupOnViewPlatformEntry(BOUNDS_LORES);
541
542 /**Normal high-res visibility entry criterion. */
543 private final WakeupOnViewPlatformEntry hiResWakeupOnViewPlatformEntry = new WakeupOnViewPlatformEntry(BOUNDS_HIRES);
544
545 /**Normal low-res visibility exit criterion. */
546 private final WakeupOnViewPlatformExit loResWakeupOnViewPlatformExit = new WakeupOnViewPlatformExit(loResWakeupOnViewPlatformEntry.getBounds());
547
548 /**Normal high-res visibility exit criterion. */
549 // private final WakeupOnViewPlatformExit hiResWakeupOnViewPlatformExit = new WakeupOnViewPlatformExit(BOUNDS_HIRES);
550
551 /**Timer wakeup for when we are waiting to set the hi-res texture; we try to do this quickly since it is close to the user. */
552 private final WakeupOnElapsedTime hiResWakeupOnElapsedTime = new WakeupOnElapsedTime(CoreConsts.MAX_INTERACTIVE_DELAY_MS/2 + Rnd.fastRnd.nextInt(CoreConsts.MAX_INTERACTIVE_DELAY_MS));
553
554 /**Timer wakeup for when we are waiting to set the lo-res texture; we can wait longer for this and induce less "polling" CPU load. */
555 private final WakeupOnElapsedTime loResWakeupOnElapsedTime = new WakeupOnElapsedTime(CoreConsts.MAX_INTERACTIVE_DELAY_MS*4 + Rnd.fastRnd.nextInt(CoreConsts.MAX_INTERACTIVE_DELAY_MS*2));
556
557 /**At initialisation. */
558 @Override
559 public final void initialize()
560 {
561 wakeupOn(loResWakeupOnViewPlatformEntry);
562 }
563
564 /**Return TRUE if object is definitely NOT visible, for optimisation purposes.
565 * Return FALSE if not sure, null is partially/peripherally visible.
566 * <p>
567 * This is used to optimise behaviours, texture fetches, etc.
568 *
569 * @return TRUE if object is definitely NOT visible,
570 * null if probably not fully visible,
571 * FALSE if definitely visible (or if not sure)
572 */
573 private Boolean isNotVisibleToUser()
574 {
575 final VirtualUniverse virtualUniverse = simpleUniverse.getLocale().getVirtualUniverse();
576 if((virtualUniverse instanceof SimpleUniverse) && (front != null))
577 {
578 // We can get the view-platform this way...
579 final SimpleUniverse su = (SimpleUniverse) virtualUniverse;
580 final Vector3d v3dUser = computeViewPlatformVWorldXYZ(su);
581 //System.out.println("USER Z = " + v3dUser.z);
582
583 // Get exhibit central Z coordinate...
584 final Transform3D t3dMe = new Transform3D(); // My transform...
585 front.getLocalToVworld(t3dMe);
586 final Vector3d v3dMe = new Vector3d(); // My centre...
587 t3dMe.get(v3dMe);
588 //System.out.println("FRONT CENTRAL Z = " + v3dMe.z);
589
590 // If the user is in front of (more -ve z) than box centre
591 // then regard this box as not visible to the user.
592 // We can only do this because the user is not permitted
593 // to turn their view sideways/backwards.
594 //
595 // This should actually be slightly conservative
596 // given that the back of each box is not interesting,
597 // and the near view-clipping plane is ahead of the user.
598 if(v3dUser.z < v3dMe.z - EXHIBIT_CSPACING_M)
599 {
600 //System.out.println("BOX NOT VISIBLE (behind user): "+index);
601 return(Boolean.TRUE);
602 }
603
604 // Establishing if exhibit is "peripherially" visible,
605 // ie should have less computational effort spent on it.
606
607 // If the distance is such that the exhibit (centre)
608 // is not fully visible
609 // then we can return null
610 // to reduce effort spent on peripheral items.
611 final Vector3d diff = new Vector3d(v3dUser);
612 diff.sub(v3dMe);
613 final double lenSq = diff.lengthSquared();
614 if(lenSq > FOG_START*FOG_START)
615 { return(null); }
616
617 // Crudely, if out of the centre of vision and not v close
618 // computed as x or y distance approaching z distance,
619 // ie about 45 degrees from view platform normal,
620 // then we can regard this as "peripheral".
621 if((lenSq > 2*EXHIBIT_CSPACING_M*EXHIBIT_CSPACING_M) &&
622 (Math.max(Math.abs(diff.x), Math.abs(diff.y)) > Math.abs(diff.z)/2))
623 { return(null); }
624
625 // Definitely fully visible, so far as we can tell.
626 return(Boolean.FALSE);
627 }
628
629 return(null); // Don't know, so assume may be partly visible.
630 }
631
632 /**Front face of exhibit box to which exhibit is applied; null initially and when not in use (not visible).
633 * Marked volatile for thread-safe lock-free access.
634 */
635 private volatile Shape3D front;
636
637 /**Load state indicator; null initially and when not in use (not visible).
638 * Marked volatile for thread-safe lock-free access.
639 */
640 private volatile Sphere indicatorSphere;
641
642 /**Colour of load state indicator; null initially and when not in use (not visible).
643 * Marked volatile for thread-safe lock-free access.
644 */
645 private volatile ColoringAttributes iSC;
646
647 /**Caption text Node; null initially and when not in use (not visible).
648 * Marked volatile for thread-safe lock-free access.
649 */
650 private volatile Group tgCaption;
651
652 /**Accept stimuli.
653 * When we are far enough from the viewer to be invisible
654 * then we set the texture to null to save rendering and memory.
655 */
656 @Override
657 public final void processStimulus(final Enumeration stimuli)
658 {
659 boolean hasEnteredHiResArea = false;
660
661 while(stimuli.hasMoreElements())
662 {
663 final WakeupCriterion w = (WakeupCriterion) stimuli.nextElement();
664
665 // Regard any activity, other than timer events,
666 // as evidence of user activity.
667 if(!(w instanceof WakeupOnElapsedTime))
668 { logic.setUserLastActive("Received stimulus"); }
669
670 // If exiting the viewable area altogether
671 // then discard all other easily-reconstucatble state
672 // to save memory and rendering CPU cycles,
673 // and only wake up on the next entry.
674 // This should be present as a possible condition
675 // in all other cases so that we can release resources.
676 //
677 // This idea to drastically reduce memory consumption
678 // was suggested by the first of the famous Punch limericks:
679 //
680 // There once was a man who said, "God
681 // Must think it exceedingly odd
682 // If he finds that this tree
683 // Continues to be
684 // When there's no one about in the Quad."
685 //
686 // Here only the vestigial Behavior of the tree
687 // exists when no one is looking!
688 if(w == loResWakeupOnViewPlatformExit)
689 {
690 // Discard all visible elements.
691 discardable.removeAllChildren();
692 // Drop references to items in the discardable group
693 // to enable GC to do its stuff...
694 indicatorSphere = null;
695 front = null;
696 tgCaption = null;
697
698 //if(IsDebug.isDebug) { System.err.println("ZAPPED VIEWABLES FOR #"+index); }
699
700 // Avoid processing any more events this round,
701 // such as timed wakeups which could refetch textures.
702 if(false && IsDebug.isDebug && stimuli.hasMoreElements())
703 {
704 logger.log("WARNING discarding excess stimuli after exiting lo-res bounds.");
705 while(stimuli.hasMoreElements())
706 { logger.log(" DISCARDING: " + stimuli.nextElement()); }
707 }
708 // Sleep until next entry into viewing space,
709 // even skipping straight into hi-res space.
710 wakeupOn(new WakeupOr(new WakeupCriterion[]{
711 hiResWakeupOnViewPlatformEntry,
712 loResWakeupOnViewPlatformEntry
713 }));
714
715 return;
716 }
717
718 // Create viewable scene elements on demand.
719 if(discardable.numChildren() == 0)
720 {
721 final BranchGroup bg = new BranchGroup();
722 bg.setCapability(BranchGroup.ALLOW_DETACH);
723 bg.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
724
725 // Place exhibit at the centre of the specified space...
726 final Texture texture = null;
727 // try { texture = logic.getThumbnailImageAsTexture(-1, false, false); }
728 // catch(final Throwable t) { t.printStackTrace(); }
729 final Appearance appearance = new Appearance();
730 appearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
731 appearance.setTexture(texture);
732 final int textureWidth = (texture == null) ? 1 : texture.getWidth();
733 final TexCoordGeneration texCoordGeneration = _createTexGen(textureWidth);
734 appearance.setTexCoordGeneration(texCoordGeneration);
735 appearance.setCapability(Appearance.ALLOW_TEXGEN_WRITE);
736 final ColoringAttributes colouring = new ColoringAttributes(1,1,1, ColoringAttributes.SHADE_FLAT);
737 appearance.setColoringAttributes(colouring);
738
739 final float boxDim = MAX_EXHIBIT_DIM_M/2;
740 final float boxZDepth = MAX_EXHIBIT_DIM_M/5;
741 final com.sun.j3d.utils.geometry.Box box =
742 new com.sun.j3d.utils.geometry.Box(boxDim, boxDim, boxZDepth,
743 appearance);
744 box.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
745 box.setCapability(Shape3D.ALLOW_APPEARANCE_READ);
746 box.setPickable(true); // Pickable to pop up exhibit details/page.
747 final String exhibitIndex = MemoryTools.intern(String.valueOf(index));
748 box.setUserData(exhibitIndex); // Set the name to be the index.
749 final Transform3D translate = new Transform3D();
750 translate.set(new Vector3f(centre3d));
751 final TransformGroup tg = new TransformGroup(translate);
752 tg.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
753 front = box.getShape(com.sun.j3d.utils.geometry.Box.FRONT);
754 front.setUserData(exhibitIndex); // Set the name to be the index.
755 front.setCapability(Shape3D.ALLOW_APPEARANCE_READ);
756 front.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
757 final Appearance frontApp = front.getAppearance();
758 frontApp.setTexture(texture);
759 frontApp.setTexCoordGeneration(texCoordGeneration);
760 frontApp.setCapability(Appearance.ALLOW_TEXTURE_READ);
761 frontApp.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
762 frontApp.setCapability(Appearance.ALLOW_TEXGEN_WRITE);
763 tg.addChild(box);
764 bg.addChild(tg);
765
766 // Create transform group for "indicator".
767 final Transform3D translateiS = new Transform3D();
768 translateiS.set(new Vector3f(centre.x - (0.55f*MAX_EXHIBIT_DIM_M), centre.y, centre.z + boxZDepth));
769 final TransformGroup tgiS = new TransformGroup(translateiS);
770 // tgiS.setCapability(TransformGroup.ALLOW_CHILDREN_WRITE);
771 // tgiS.setCapability(TransformGroup.ALLOW_CHILDREN_EXTEND);
772 bg.addChild(tgiS);
773 // Create and save ref to indicator.
774 indicatorSphere = new Sphere(0.04f*MAX_EXHIBIT_DIM_M);
775 indicatorSphere.setCapability(Primitive.ENABLE_APPEARANCE_MODIFY);
776 final Appearance iSApp = new Appearance();
777 /*final ColoringAttributes */ iSC = new ColoringAttributes(TEXT_ALT_COLOUR, ColoringAttributes.SHADE_FLAT);
778 iSC.setCapability(ColoringAttributes.ALLOW_COLOR_WRITE);
779 iSApp.setColoringAttributes(iSC);
780 indicatorSphere.setAppearance(iSApp);
781 tgiS.addChild(indicatorSphere);
782
783 // Add caption's TransformGroup.
784 // Set text node ASAP when exhibit is visible.
785 final Transform3D translateCaption = new Transform3D();
786 final float captionYOffset = (0.6f*MAX_EXHIBIT_DIM_M);
787 translateCaption.set(new Vector3f(centre.x - (0.5f*MAX_EXHIBIT_DIM_M), centre.y - captionYOffset, centre.z + boxZDepth));
788 tgCaption = new TransformGroup(translateCaption);
789 tgCaption.setCapability(Group.ALLOW_CHILDREN_READ);
790 tgCaption.setCapability(Group.ALLOW_CHILDREN_WRITE);
791 tgCaption.setCapability(Group.ALLOW_CHILDREN_EXTEND);
792 bg.addChild(tgCaption);
793
794 bg.compile();
795 discardable.addChild(bg);
796 }
797
798 final Boolean notVisible = isNotVisibleToUser();
799 final boolean notAtAllVisible = Boolean.TRUE.equals(notVisible);
800 final boolean notFullyVisible = !Boolean.FALSE.equals(notVisible);
801
802 //if(IsDebug.isDebug && !(w instanceof WakeupOnElapsedTime)) { System.err.println("NOTVISIBILE="+notVisible+" for #"+index); }
803
804 // Short-cuts for later...
805 final Appearance app = front.getAppearance();
806 // final ColoringAttributes iSC = indicatorSphere.getAppearance().getColoringAttributes();
807
808 // Try to create caption if not yet done,
809 // perhaps because the name was not immediately available.
810 if(tgCaption.numChildren() == 0)
811 {
812 final Name.ExhibitFull exhibitName = logic.getExhibitName(index);
813 if(exhibitName != null)
814 {
815 // Make caption.
816 // Always start with at least the main component...
817 final String captionFullText = exhibitName.getShortName().toString();
818 final Text2D caption = _makeCaptionTexture(captionFullText);
819 final String exhibitIndex = MemoryTools.intern(String.valueOf(index));
820 caption.setUserData(exhibitIndex); // Set the name to be the exhibit index.
821 caption.setPickable(true);
822
823 // Make (monocased) index/initial letter.
824 final char indexLetter = Character.toLowerCase(captionFullText.charAt(0));
825 // Create a text node with the index value...
826 final Shape3D indexTextShape = _makeIndexLetter(indexLetter);
827
828 final BranchGroup cbg = new BranchGroup();
829 // Allow this entire group to be discarded.
830 // cbg.setCapability(BranchGroup.ALLOW_DETACH);
831 cbg.addChild(caption);
832 cbg.addChild(indexTextShape);
833 cbg.compile();
834 tgCaption.addChild(cbg);
835 }
836 }
837
838 // Flicker the indicator while loading the image...
839 iSC.setColor(Rnd.fastRnd.nextBoolean() ? TEXT_ALT_COLOUR : (!notFullyVisible ? TEXT_ALT3_COLOUR : TEXT_ALT2_COLOUR));
840
841 // If entering lo-res area
842 // or receiving a wakeup to try to refetch lo-res texture
843 // (or receiving initial wakeup in reduced lo-res area)
844 // then set the texture to the lo-res texture
845 // to save memory and CPU cycles,
846 // and only wake up on the next entry
847 // or periodically to try to (re)fetch the texture...
848 if((w == loResWakeupOnViewPlatformEntry) ||
849 (w == loResWakeupOnElapsedTime))
850 {
851 // Once started on hi-res texture fetch,
852 // don't interrupt it except by an exit.
853 if(hasEnteredHiResArea) { continue; }
854
855 Texture tnTexture = null;
856 // Keep postponing texture fetch if out of sight....
857 if(!notAtAllVisible)
858 {
859 try {
860 tnTexture = logic.getThumbnailImageAsTexture(index, false, notFullyVisible); }
861 catch(final Throwable t) { t.printStackTrace(); }
862 }
863
864 //if(IsDebug.isDebug && (w == loResWakeupOnViewPlatformEntry)) { System.err.println("LO-RES ENTRY FOR " + logic.getExhibitName(index) + ", tnTexture:" + tnTexture + ", notVisible:"+notVisible); }
865 //if(IsDebug.isDebug) { System.err.println("LO-RES TICK FOR " + logic.getExhibitName(index) + ", tnTexture:" + tnTexture + ", notVisible:"+notVisible); }
866
867 // Set the texture to the lo-res version
868 // if immediately available...
869 if(tnTexture != null)
870 {
871 // app.setTexCoordGeneration(_createTexGen(tnTexture.getWidth()));
872 app.setTexture(tnTexture);
873
874 // Show that we have loaded the texture fully.
875 iSC.setColor(TEXT_OK_COLOUR);
876
877 // Sleep until next entry into hi-res viewing space
878 // or exit even from lo-res viewing space...
879 wakeupOn(new WakeupOr(new WakeupCriterion[]{
880 hiResWakeupOnViewPlatformEntry,
881 loResWakeupOnViewPlatformExit
882 }));
883 }
884 // Else also set a timer to try again to get texture.
885 else
886 {
887 wakeupOn(new WakeupOr(new WakeupCriterion[]{
888 hiResWakeupOnViewPlatformEntry,
889 loResWakeupOnViewPlatformExit,
890 loResWakeupOnElapsedTime
891 }));
892 }
893 continue;
894 }
895
896 // If entering hi-res area
897 // or on receiving a wakeup to try to refetch hi-res texture
898 // then set the texture to the hi-res texture
899 // and only wake up on the next entry/exit of lo-res space
900 // or periodically to (re)try to fetch hi-res texture...
901 if((w == hiResWakeupOnViewPlatformEntry) ||
902 (w == hiResWakeupOnElapsedTime))
903 {
904 // Indicate that we're processing hi-res texture now.
905 hasEnteredHiResArea = true;
906
907 Texture tnTexture = null;
908 // Keep postponing texture fetch if out of sight....
909 if(!notAtAllVisible)
910 {
911 try { tnTexture = logic.getThumbnailImageAsTexture(index, true, notFullyVisible); }
912 catch(final Throwable t) { t.printStackTrace(); }
913 }
914
915 //if(IsDebug.isDebug && (w == hiResWakeupOnViewPlatformEntry)) { System.err.println("HI-RES ENTRY FOR " + logic.getExhibitName(index) + ", tnTexture:" + tnTexture + ", notVisible:"+notVisible); }
916 //if(IsDebug.isDebug && (w != hiResWakeupOnViewPlatformEntry)) { System.err.println("HI-RES TICK FOR " + logic.getExhibitName(index) + ", tnTexture:" + tnTexture + ", notVisible:"+notVisible); }
917
918 // Set the texture to the hi-res version
919 // if immediately available...
920 if(tnTexture != null)
921 {
922 // app.setTexCoordGeneration(_createTexGen(tnTexture.getWidth()));
923 app.setTexture(tnTexture);
924
925 // Show that we have loaded the texture fully.
926 iSC.setColor(TEXT_OK_COLOUR);
927
928 // Sleep until next entry into lo-res viewing space
929 // or exit even from lo-res viewing space...
930 wakeupOn(/*new WakeupOr(new WakeupCriterion[]{
931 loResWakeupOnViewPlatformEntry, */
932 loResWakeupOnViewPlatformExit /* ,
933 }) */ );
934 }
935 // Else also set a timer to try again to get texture.
936 else
937 {
938 // If we don't have the hi-res texture
939 // then (re)use the lo-res one in the interim
940 // if none is currently set.
941 // Keep postponing texture fetch if out of sight....
942 if((app.getTexture() == null) && !notAtAllVisible)
943 {
944 try { tnTexture = logic.getThumbnailImageAsTexture(index, false, notFullyVisible); }
945 catch(final Throwable t) { t.printStackTrace(); }
946 // app.setTexCoordGeneration(_createTexGen(tnTexture.getWidth()));
947 app.setTexture(tnTexture);
948 }
949
950 wakeupOn(new WakeupOr(new WakeupCriterion[]{
951 // loResWakeupOnViewPlatformEntry,
952 loResWakeupOnViewPlatformExit,
953 hiResWakeupOnElapsedTime
954 }));
955 }
956 continue;
957 }
958
959 logger.log("WARNING: unrecognised stimulus: " + w);
960 }
961 }
962 };
963
964 // This result will always directly contain
965 // the behaviour and scheduling bounds.
966 final BranchGroup result = new BranchGroup();
967 result.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
968 result.addChild(discardable);
969 appearanceBehaviour.setSchedulingBounds(new BoundingSphere(centre3d, EXHIBIT_VISIBLE_M));
970 result.addChild(appearanceBehaviour);
971
972 if(false && IsDebug.isDebug)
973 {
974 // Create a text node with the index value...
975 final Text3D bannerText = new Text3D(new Font3D(new Font("Serif", Font.PLAIN, 1), new FontExtrusion()),
976 String.valueOf(index),
977 centre,
978 Text3D.ALIGN_CENTER,
979 Text3D.PATH_RIGHT);
980 final Appearance textAppearance = new Appearance();
981 final ColoringAttributes textColouring = new ColoringAttributes(1,1,0, ColoringAttributes.SHADE_FLAT);
982 textAppearance.setColoringAttributes(textColouring);
983 // textAppearance.setTexture(texture);
984 // textAppearance.setTexCoordGeneration(new TexCoordGeneration());
985 final Shape3D textShape = new Shape3D(bannerText, textAppearance);
986 result.addChild(textShape);
987 }
988
989 result.compile();
990 return(result);
991 }
992
993 /**Minimum number of characters that we will show in a caption for comprehensibility; non-negative. */
994 private static final int MIN_CAPTION_CHARS = 10;
995
996 /**Maximum number of chars (excluding any trailer) so far found to fit in a caption; no less than MIN_CAPTION_CHARS.
997 * Suitable for thread-safe lockless access.
998 */
999 private static final AtomicInteger maxCaptionCharsSoFar = new AtomicInteger(MIN_CAPTION_CHARS);
1000
1001 /**Suffix to add to any trimmed caption. */
1002 private static final String TRIMMED_CAPTION_SUFFIX = "...";
1003
1004 /**Makes the caption for an exhibit as a texture; never null.
1005 * Tries to reuse textures, etc, to save time and space.
1006 *
1007 * @param captionFullText printable-ASCII string; never null
1008 */
1009 private static Text2D _makeCaptionTexture(final String captionFullText)
1010 {
1011 //final long captionBuildStart = System.currentTimeMillis();
1012
1013 String workingText = captionFullText;
1014 Text2D caption = null;
1015 // Consider trimming caption text only if text longer than minimum.
1016 if(workingText.length() > MIN_CAPTION_CHARS)
1017 {
1018 // Note if we have had to shorten the caption to fit...
1019 boolean shortened; // = false;
1020
1021 // Intersection point to check for caption being too long.
1022 final Point3d lengthCheckPoint = new Point3d(2*MAX_EXHIBIT_DIM_M, 0, 0);
1023
1024 // If text is longer than max chars yet found to fit
1025 // then assume that we will need to truncate to at most that length.
1026 // (If that is too short then bump up max chars notion.)
1027 // Go on increasing the length until the caption is just too long...
1028 final int mc1 = maxCaptionCharsSoFar.get();
1029 if(workingText.length() > mc1)
1030 {
1031 final int fullLength = captionFullText.length();
1032 for(int len = mc1; len <= workingText.length(); ++len)
1033 {
1034 shortened = len < fullLength;
1035 final String s = workingText.substring(0, len);
1036 String trimmed = s;
1037 if(shortened) { trimmed += TRIMMED_CAPTION_SUFFIX; }
1038 final Text2D trialCaption = _makeRawCaptionText2D(trimmed);
1039 final boolean tooLong = trialCaption.getBounds().intersect(lengthCheckPoint);
1040
1041 // We stop as soon as the trial caption is too long
1042 // and make that our working text...
1043 if(tooLong)
1044 {
1045 workingText = s; // Will need at least one char trimmed...
1046 break;
1047 }
1048
1049 // If this trial caption was unexpectedly NOT too long
1050 // then ensure that we bump up the max limit to at least
1051 // the current length (+1) without race problems.
1052 for( ; ; )
1053 {
1054 final int current = maxCaptionCharsSoFar.get();
1055 final int next = Math.max(current, len+1);
1056 if(maxCaptionCharsSoFar.compareAndSet(current, next))
1057 { break; }
1058 }
1059 //System.err.println("*** maxCaptionCharsSoFar = " + maxCaptionCharsSoFar);
1060 }
1061
1062 // Now work down towards the minimum acceptable length
1063 // stopping when we get a trimmed version that just fits...
1064 final StringBuilder sb = new StringBuilder(workingText);
1065 for(int len = workingText.length(); len >= MIN_CAPTION_CHARS; --len)
1066 {
1067 shortened = len < fullLength;
1068 sb.setLength(len); // Trim off any surplus...
1069 if(shortened) { sb.append(TRIMMED_CAPTION_SUFFIX); }
1070 // Eliminate duplicates from memory...
1071 caption = _makeRawCaptionText2D(MemoryTools.intern(sb.toString()));
1072 final boolean tooLong = caption.getBounds().intersect(lengthCheckPoint);
1073 // When not too long then we can stop!
1074 if(!tooLong) { break; }
1075 }
1076 }
1077 }
1078 // (Re)build caption if necessary...
1079 if(caption == null)
1080 { caption = _makeRawCaptionText2D(MemoryTools.intern(workingText)); }
1081
1082 // caption.setAppearance(TEXT_BG_APPEARANCE);
1083
1084 //final long captionBuildEnd = System.currentTimeMillis();
1085 //System.err.println("Caption text build/trim time (ms): " + (captionBuildEnd - captionBuildStart) + " for " + captionFullText);
1086 return(caption);
1087 }
1088
1089 /**Make raw caption Text2D from the given text; never null. */
1090 private static Text2D _makeRawCaptionText2D(final String sb)
1091 {
1092 return new Text2D(sb,
1093 TEXT_COLOUR,
1094 "Serif",
1095 Math.round(20 * MAX_EXHIBIT_DIM_M),
1096 Font.PLAIN);
1097 }
1098
1099 /**Private geometry cache for _makeIndexLetter; never null.
1100 * Big enough to hold geometry for all ASCII chars up to 'z' by index.
1101 * <p>
1102 * Accessed under a lock on this array for thread-safety.
1103 */
1104 private static final Text3D _makeIndexLetter_geomCache[] = new Text3D[1 + 'z'];
1105
1106 /**Makes an index letter bottom-right aligned to (0,0,0); never null.
1107 * We try to cache the geometry of the text to save time and memory.
1108 *
1109 * @param indexLetter printable ASCII character (up to 'z')
1110 */
1111 private static Shape3D _makeIndexLetter(final char indexLetter)
1112 {
1113 assert((indexLetter >= ' ') && (indexLetter < _makeIndexLetter_geomCache.length));
1114
1115 synchronized(_makeIndexLetter_geomCache)
1116 {
1117 Text3D cached = _makeIndexLetter_geomCache[indexLetter];
1118 if(cached == null)
1119 {
1120 // Compute geometry since not yet done for this letter.
1121 _makeIndexLetter_geomCache[indexLetter] = cached =
1122 new Text3D(new Font3D(new Font("Serif", Font.PLAIN, 1), new FontExtrusion()),
1123 MemoryTools.intern("" + indexLetter),
1124 new Point3f(),
1125 Text3D.ALIGN_LAST,
1126 Text3D.PATH_RIGHT);
1127 }
1128 final Shape3D indexTextShape = new Shape3D(cached, TEXT_APPEARANCE);
1129 return(indexTextShape);
1130 }
1131 }
1132
1133 /**Compute the view-platform location (x,y,z) in virtual-world coordinates; never null. */
1134 private static Vector3d computeViewPlatformVWorldXYZ(final SimpleUniverse su)
1135 {
1136 final Transform3D t3dUser = new Transform3D();
1137 su.getViewer().getView().getUserHeadToVworld(t3dUser);
1138 final Vector3d v3dUser = new Vector3d();
1139 t3dUser.get(v3dUser);
1140 return(v3dUser);
1141 }
1142
1143 /**Cache for _createTexGen; never null.
1144 * Caches values for up to the standard thumbnail dimensions,
1145 * created on first use.
1146 * <p>
1147 * This is likely to remain largely empty
1148 * since we are only likely to use the slots corresponding to
1149 * textures that are powers of two.
1150 */
1151 private static final TexCoordGeneration _createTexGen_cache[] = new TexCoordGeneration[ThreeDLogic.TN_STD_IMAGE_DIM];
1152
1153 /**Build a tex-gen that maps a texture of the given width across our exhibit box front face; never null.
1154 * This assumes that the caller will never modify the result,
1155 * and thus instances can be safely cached and reused.
1156 *
1157 * @param textureWidth positive power of two
1158 */
1159 private synchronized static TexCoordGeneration _createTexGen(final int textureWidth)
1160 {
1161 assert(textureWidth > 0);
1162
1163 TexCoordGeneration result; // = null;
1164 // Check the cache.
1165 final boolean canUseCache = textureWidth <= ThreeDLogic.TN_STD_IMAGE_DIM;
1166 if(canUseCache)
1167 {
1168 result = _createTexGen_cache[textureWidth - 1];
1169 if(result != null) { return(result); }
1170 }
1171
1172 result = new TexCoordGeneration();
1173 result.setPlaneS(new Vector4f(1.0f / textureWidth, 0, 0, 0.5f));
1174 result.setPlaneT(new Vector4f(0, 1.0f / textureWidth, 0, 0.5f));
1175
1176 if(canUseCache)
1177 {
1178 // Cache the computed value if possible.
1179 _createTexGen_cache[textureWidth - 1] = result;
1180 }
1181
1182 return(result);
1183 }
1184
1185 /**If true then lay out exhibits in 2D grid, else use 3D layout. */
1186 private static final boolean USE_2D_LAYOUT = false;
1187
1188 /**Compute width/height (X/Y dimension) of exhibit grid given exhibit count. */
1189 private static int computeGridXYDim(final int exhibitCount)
1190 {
1191 if(USE_2D_LAYOUT)
1192 { return((int) Math.ceil(Math.sqrt(exhibitCount))); }
1193 else
1194 { return((int) Math.ceil(Math.cbrt(exhibitCount))); }
1195 }
1196
1197 /**Compute depth (Z dimension) of exhibit grid given exhibit count. */
1198 private static int computeGridZDim(final int exhibitCount)
1199 {
1200 if(USE_2D_LAYOUT) /* Of fixed (1) depth for 2D layout. */
1201 { return(1); }
1202 else // For a 3D layout this is thre same as the X/Y dimension. */
1203 { return(computeGridXYDim(exhibitCount)); }
1204 }
1205
1206 /**Maximum (most positive) permitted view-platform-centre X value to avoid losing sight of exhibits. */
1207 private static final float MAX_VPC_X = 0;
1208
1209 /**Maximum (most positive) permitted view-platform-centre Y value to avoid losing sight of exhibits. */
1210 private static final float MAX_VPC_Y = MAX_VPC_X;
1211
1212 /**Maximum (most positive) permitted view-platform-centre Z value to avoid losing sight of exhibits. */
1213 private static final float MAX_VPC_Z = EXHIBIT_VISIBLE_M/2;
1214
1215 /**Compute minimum (most negative) permitted view-platform-centre X value to avoid losing sight of exhibits. */
1216 private float computeMinVPCX()
1217 {
1218 return(Math.min(MAX_VPC_X - EXHIBIT_CSPACING_M * (2 + computeGridXYDim(currentSet.exhibitCount)), 0));
1219 }
1220
1221 /**Compute minimum (most negative) permitted view-platform-centre Y value to avoid losing sight of exhibits. */
1222 private float computeMinVPCY()
1223 {
1224 return(Math.min(MAX_VPC_Y - EXHIBIT_CSPACING_M * (1 + computeGridXYDim(currentSet.exhibitCount)), 0));
1225 }
1226
1227 /**Compute minimum (most negative) permitted view-platform-centre Z value to avoid losing sight of exhibits. */
1228 private float computeMinVPCZ()
1229 {
1230 return(Math.min(MAX_VPC_Z - EXHIBIT_CSPACING_M * (2 + computeGridZDim(currentSet.exhibitCount)), -EXHIBIT_CSPACING_M));
1231 }
1232
1233 /**Make the BranchGroup containing the current set of exhibits; never null.
1234 * This should <em>replace</em> any previous exhibit BranchGroup.
1235 *
1236 * @param galleryBasicMetaData new exhibit set meta-data
1237 * @param topRightFront origin/offset of this exhibit set; never null
1238 *
1239 * @return non-null compiled BranchGroup containing the exhibits,
1240 * with BranchGroup.ALLOW_DETACH capability set
1241 */
1242 private BranchGroup makeExhibitsScene(final LightweightMetaDataFetchInterface.GalleryBasicMetaData galleryBasicMetaData,
1243 final Point3f topRightFront)
1244 {
1245 final BranchGroup result = new BranchGroup();
1246
1247 // Allow for removal (and replacement) of this exhibit-based scene
1248 // when the exhibit set changes.
1249 result.setCapability(BranchGroup.ALLOW_DETACH);
1250 result.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
1251
1252 final float offsetX = topRightFront.x;
1253 final float offsetY = topRightFront.y - EXHIBIT_CSPACING_M / 2;
1254 final float offsetZ = topRightFront.z - EXHIBIT_CSPACING_M / 2;
1255
1256 if(USE_2D_LAYOUT)
1257 {
1258 // Compute length of one X/Y grid edge; Z depth is 1.
1259 final int gridSize = computeGridXYDim(galleryBasicMetaData.exhibitCount);
1260
1261 // Make a solid grid: compute the exhibit index as we go.
1262 // Have the index weave from left to right to left, top to bottom.
1263 int index = 0;
1264 for(int y = 0; y < gridSize; ++y)
1265 {
1266 final float yo = offsetY - (y * EXHIBIT_CSPACING_M);
1267 final boolean reverseX = ((y & 1) != 0);
1268 for(int x = (reverseX ? gridSize : -1); reverseX ? (--x >= 0) : (++x < gridSize); )
1269 {
1270 final float xo = offsetX - (x * EXHIBIT_CSPACING_M);
1271 if(index >= galleryBasicMetaData.exhibitCount) { break; }
1272 final Node node = makeExhibitSpace(new Point3f(xo, yo, offsetZ), index++);
1273 result.addChild(node);
1274 }
1275
1276 // Be nice to the rendering thread, etc...
1277 Thread.yield();
1278 }
1279 }
1280 else
1281 {
1282 // Compute length of one (Z) grid edge; others assumed to be same.
1283 final int gridSize = computeGridZDim(galleryBasicMetaData.exhibitCount);
1284
1285 // Make a solid grid: compute the exhibit index as we go.
1286 // Have index weave right-left-right, top-bottom-top.
1287 int index = 0;
1288 boolean yUp = false; // Go upwards.
1289 boolean reverseX = false; // Go right-to-left.
1290 for(int z = 0; z < gridSize; ++z)
1291 {
1292 final float zo = offsetZ - (z * EXHIBIT_CSPACING_M);
1293 // Offset adjacent planes a little.
1294 final float skewX = ((float)Math.sin(z)) * (MAX_EXHIBIT_DIM_M/2);
1295 final float skewY = ((float)Math.cos(z)) * (MAX_EXHIBIT_DIM_M/2);
1296 for(int y = (yUp ? gridSize : -1); yUp ? (--y >= 0) : (++y < gridSize); )
1297 {
1298 final float yo = offsetY - (y * EXHIBIT_CSPACING_M) - skewY;
1299 for(int x = (reverseX ? gridSize : -1); reverseX ? (--x >= 0) : (++x < gridSize); )
1300 {
1301 final float xo = offsetX - (x * EXHIBIT_CSPACING_M) - skewX;
1302 if(index >= galleryBasicMetaData.exhibitCount) { break; }
1303 final Node node = makeExhibitSpace(new Point3f(xo, yo, zo), index++);
1304 result.addChild(node);
1305 }
1306 reverseX = !reverseX;
1307
1308 // Be nice to the rendering thread, etc...
1309 Thread.yield();
1310 }
1311 yUp = !yUp;
1312 }
1313 }
1314
1315 result.compile();
1316 return(result);
1317 }
1318
1319 /**Initialise user's location, etc. */
1320 private void initUserControls(final SimpleUniverse u,
1321 final BranchGroup exhibitsBranchGroup)
1322 {
1323 // Cap the frame rate to save a CPU cycle or two...
1324 final View view = u.getViewer().getView();
1325 view.setMinimumFrameCycleTime(10); // 100Hz
1326
1327 // This will move the ViewPlatform back a bit so that
1328 // the objects in the scene can be viewed.
1329 final ViewingPlatform vp = u.getViewingPlatform();
1330 vp.setNominalViewingTransform();
1331
1332 // Allow us to get the location of the user/viewer at run-time.
1333 view.setUserHeadToVworldEnable(true);
1334
1335 // Harmonise the view frustrum clipping with our use of fog, etc.
1336 view.setBackClipDistance(EXHIBIT_VISIBLE_M * 1.1);
1337 // View exhibits almost with your nose pressed up agains them (~10cm).
1338 view.setFrontClipDistance(EXHIBIT_VISIBLE_M * 0.01);
1339 if(IsDebug.isDebug) { System.out.println("[Field of view (radians) = " + view.getFieldOfView() + ".]"); }
1340
1341 // Now set up mouse-based control of the viewing platform...
1342 final TransformGroup viewPlatformTransform = vp.getViewPlatformTransform();
1343 final Behavior navigatorBehavior1 = new MouseTranslate(viewPlatformTransform);
1344 final BoundingSphere boundingSphere = new BoundingSphere(new Point3d(), Double.POSITIVE_INFINITY);
1345 navigatorBehavior1.setSchedulingBounds(boundingSphere);
1346 final Behavior navigatorBehavior2 = new MouseZoom(viewPlatformTransform);
1347 navigatorBehavior2.setSchedulingBounds(boundingSphere);
1348 final BranchGroup scene = new BranchGroup();
1349 scene.addChild(navigatorBehavior1);
1350 scene.addChild(navigatorBehavior2);
1351 scene.compile();
1352 vp.addChild(scene);
1353
1354 // Set up picking...
1355 final PickCanvas pickCanvas = new PickCanvas(canvas3D, exhibitsBranchGroup);
1356 pickCanvas.setMode(PickInfo.PICK_BOUNDS);
1357 pickCanvas.setFlags(PickInfo.NODE);
1358 canvas3D.addMouseListener(new MouseListener()
1359 {
1360 public void mouseClicked(final MouseEvent e)
1361 {
1362 // Regards user as active.
1363 logic.setUserLastActive("Mouse click");
1364
1365 // Search all matching selected visible nodes, closest first.
1366 pickCanvas.setShapeLocation(e);
1367 final PickInfo pickInfos[] = pickCanvas.pickAllSorted();
1368 if(pickInfos == null) { return; /* Nothing to see here; move along please. */ }
1369 for(final PickInfo pickInfo : pickInfos)
1370 {
1371 //System.out.println("pickInfo = " + pickInfo);
1372 if(pickInfo == null) { continue; }
1373
1374 // Filter out out-of-sight objects.
1375 final double closestDistance = pickInfo.getClosestDistance();
1376 if(Double.isNaN(closestDistance)) { continue; }
1377 if(!(closestDistance <= EXHIBIT_VISIBLE_M)) { continue; }
1378
1379 final Node node = pickInfo.getNode();
1380 //System.out.println("pickInfo.getNode() = " + node);
1381 if(node == null) { continue; }
1382
1383 // Try to retrieve the exhibit number from the node name.
1384 final String name = (String) node.getUserData();
1385 //System.out.println("node.getName() = " + name);
1386 if(name == null) { continue; }
1387
1388 // Reject unparseable or out-of-bounds exhibit index.
1389 int exhibitNumber = -1;
1390 try { exhibitNumber = Integer.parseInt(name, 10); }
1391 catch(final NumberFormatException nfe) { /* Ignore. */ }
1392 if(exhibitNumber < 0) { continue; }
1393 if(exhibitNumber >= currentSet.exhibitCount) { continue; }
1394
1395 logger.log("Selected exhibit #" + exhibitNumber);
1396 final Name.ExhibitFull exhibitName = logic.getExhibitName(exhibitNumber);
1397 // If we can't get the exhibit name (yet)
1398 // then ignore this pick for now...
1399 if(!ExhibitName.validNameSyntax(exhibitName)) { return; }
1400
1401 logger.log("Selected exhibit " + ExhibitName.getFileComponent(exhibitName));
1402
1403 // If we can, then show exhibit in browser.
1404 if((logic.bs != null) &&
1405 logic.bs.isWebBrowserSupported())
1406 {
1407 // Try to show exhibit page in browser...
1408 try
1409 {
1410 // final String rrURL = WebUtils.makeCatPageRRURL(exhibitName, WebConsts.F_secondary_generated_HTML_suffix);
1411 final String rrURL = ("/_c/" + exhibitName + WebConsts.F_secondary_generated_HTML_suffix);
1412 if(logic.bs.showDocument(new URL(logic.bs.getCodeBase(), rrURL)))
1413 { return; /* Success! */ }
1414 }
1415 catch(final Exception be)
1416 {
1417 logger.log("Problem showing exhibit page: " + be.getMessage());
1418 return; // Failed.
1419 }
1420 }
1421
1422 logger.log("Could not show selected exhibit in browser: " + ExhibitName.getFileComponent(exhibitName));
1423 return; // Failed.
1424 }
1425 }
1426
1427 public void mousePressed(final MouseEvent e)
1428 { /* Ignore this event. */ }
1429
1430 public void mouseReleased(final MouseEvent e)
1431 { /* Ignore this event. */ }
1432
1433 public void mouseEntered(final MouseEvent e)
1434 { /* Ignore this event. */ }
1435
1436 public void mouseExited(final MouseEvent e)
1437 { /* Ignore this event. */ }
1438 });
1439 }
1440
1441
1442 // /**Make the standard insets to wrap round most components.
1443 // * Note that the returned Insets() object is mutable.
1444 // */
1445 // private static Insets makeStandardInsets()
1446 // { return(new Insets(2, 2, 2, 2)); }
1447
1448 /**Creates and initialises a status bar. */
1449 static private JLabel createStatusBar()
1450 {
1451 final JLabel sbar = new JLabel("Ready");
1452 sbar.setBorder(BorderFactory.createEtchedBorder());
1453 return(sbar);
1454 }
1455
1456 /**This method acts as the Action handler delegate for all the actions. */
1457 public void actionPerformed(final ActionEvent evt)
1458 {
1459 final String command = evt.getActionCommand();
1460
1461 // // Compare the action command to the known actions,
1462 // // most-frequent first for efficiency.
1463 // if(command.equals(aboutAction.getActionCommand()))
1464 // {
1465 // // The "About" action was invoked...
1466 // JOptionPane.showMessageDialog(this, aboutAction.getLongDescription(), aboutAction.getShortDescription(), JOptionPane.INFORMATION_MESSAGE);
1467 // }
1468 // else if(command.equals(exitAction.getActionCommand()))
1469 // {
1470 // // The "Exit" action was invoked...
1471 // // Shut down unless vetoed...
1472 // try { shutdown(); }
1473 // catch(final UnsupportedOperationException e) { }
1474 // }
1475 // else
1476 {
1477 logger.log("Unexpected command: " + command);
1478 }
1479
1480 // Note user activity...
1481 logic.setUserLastActive("actionPeformed: " + command);
1482 }
1483
1484 // /**This adapter is constructed to handle mouse-over component events. */
1485 // private static final class MouseHandler extends MouseAdapter
1486 // {
1487 // private JLabel label;
1488 // private String oldMsg;
1489 //
1490 // /**Adaptor constructor.
1491 // * @param label the JLabel which will recieve value of the
1492 // * Action.LONG_DESCRIPTION key
1493 // */
1494 // public MouseHandler(final JLabel label)
1495 // {
1496 // setLabel(label);
1497 // oldMsg = label.getText();
1498 // }
1499 //
1500 // public void setLabel(final JLabel label)
1501 // {
1502 // this.label = label;
1503 // }
1504 //
1505 // public void mouseEntered(final MouseEvent evt)
1506 // {
1507 // if(evt.getSource() instanceof AbstractButton)
1508 // {
1509 // AbstractButton button = (AbstractButton) evt.getSource();
1510 // Action action = button.getAction(); // getAction is new in JDK 1.3
1511 // if(action != null)
1512 // {
1513 // oldMsg = label.getText();
1514 // String message = (String) action.getValue(Action.LONG_DESCRIPTION);
1515 // label.setText(message);
1516 // }
1517 // }
1518 // }
1519 //
1520 // public void mouseExited(final MouseEvent evt)
1521 // {
1522 // label.setText(oldMsg);
1523 // }
1524 // }
1525
1526
1527 // /**ActionListener invoked by pollUI; never null. */
1528 // private final ActionListener pollAL;
1529
1530 /**Poll UI (in AWT/Swing thread, ie Swing-safe).
1531 * This routine notices when the exhibit set changes
1532 * and rebuilds the exhibit part of the scene graph to suit.
1533 */
1534 private void pollUI(/*final ActionEvent e*/)
1535 {
1536 // If the interface is not active
1537 // then save resources here and at the server...
1538 if(logic.userInactive())
1539 {
1540 logger.log("Sleeping...");
1541 return;
1542 }
1543
1544 // pollAL.actionPerformed(e);
1545
1546 final LightweightMetaDataFetchInterface.GalleryBasicMetaData galleryBasicMetaData = logic.getGalleryBasicMetaData();
1547 if(galleryBasicMetaData.exhibitSetHash != currentSet.exhibitSetHash)
1548 {
1549 logger.log("Gallery has "+galleryBasicMetaData.exhibitCount+" exhibits...");
1550
1551 // Construct new screen graph for exhibits and replace old one.
1552 // Top/right/front at approx (0,0,0).
1553 final BranchGroup newExhitbitBranchGroup = makeExhibitsScene(galleryBasicMetaData,
1554 new Point3f());
1555 // newExhitbitBranchGroup.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
1556
1557 // Remove old exhibit scene graph.
1558 exhibitsBranchGroup.removeAllChildren();
1559 // Add new (pre-compiled) graph.
1560 exhibitsBranchGroup.addChild(newExhitbitBranchGroup);
1561
1562 // Remember new metadata.
1563 currentSet = galleryBasicMetaData;
1564
1565 // Reset slider boundaries...
1566 // Round these boundaries inwards
1567 // so that extreme slider values are just within the viewing bounds.
1568 sliderX.setMinimum((int) Math.ceil(computeMinVPCX()));
1569 sliderX.setMaximum((int) Math.floor(MAX_VPC_X));
1570 sliderY.setMinimum((int) Math.ceil(computeMinVPCY()));
1571 sliderY.setMaximum((int) Math.floor(MAX_VPC_Y));
1572 sliderZ.setMinimum((int) Math.ceil(computeMinVPCZ()));
1573 sliderZ.setMaximum((int) Math.floor(MAX_VPC_Z));
1574 }
1575
1576 // Coerce view-platform centre back into allowed range
1577 // if straying out of sight of the exhibits.
1578 final SimpleUniverse su = simpleUniverse;
1579 if(su != null)
1580 {
1581 final Vector3d vpcLocation = computeViewPlatformVWorldXYZ(su);
1582 //if(IsDebug.isDebug) { logger.log("ViewPlatform location is: " + vpcLocation); }
1583 boolean resetVP = false;
1584 if((vpcLocation.x > MAX_VPC_X) || (vpcLocation.x < computeMinVPCX()))
1585 {
1586 resetVP = true;
1587 }
1588 else if((vpcLocation.y > MAX_VPC_Y) || (vpcLocation.y < computeMinVPCY()))
1589 {
1590 resetVP = true;
1591 }
1592 else if((vpcLocation.z > MAX_VPC_Z) || (vpcLocation.z < computeMinVPCZ()))
1593 {
1594 resetVP = true;
1595 }
1596
1597 // OK, take account of out-of-bounds view platform..
1598 if(resetVP)
1599 {
1600 // Bring user back to where they can see things...
1601 if(IsDebug.isDebug) { logger.log("Forcing out-of-bounds ViewPlatform from: " + vpcLocation); }
1602 su.getViewingPlatform().setNominalViewingTransform();
1603
1604 // final TransformGroup viewPlatformTransform = su.getViewingPlatform().getViewPlatformTransform();
1605 // Transform3D View_Transform3D = new Transform3D();
1606 // viewPlatformTransform.getTransform(View_Transform3D);
1607 // View_Transform3D.setTranslation(vpcLocation);
1608 // viewPlatformTransform.setTransform(View_Transform3D);
1609 }
1610
1611 // Update position on sliders if out of sync.
1612 final int xRound = (int) Math.round(vpcLocation.x);
1613 if(sliderX.getValue() != xRound) { sliderX.setValue(xRound); }
1614 final int yRound = (int) Math.round(vpcLocation.y);
1615 if(sliderY.getValue() != yRound) { sliderY.setValue(yRound); }
1616 final int zRound = (int) Math.round(vpcLocation.z);
1617 if(sliderZ.getValue() != zRound) { sliderZ.setValue(zRound); }
1618 }
1619 }
1620
1621 /**What exhibit set are we currently displaying; never null.
1622 * When this changes we reconstruct our scene graph.
1623 * <p>
1624 * Marked volatile for thread-safe lock-free access.
1625 * <p>
1626 * Initially an empty set of exhibits.
1627 */
1628 private volatile LightweightMetaDataFetchInterface.GalleryBasicMetaData currentSet = LightweightMetaDataFetchInterface.GalleryBasicMetaData.EMPTY;
1629
1630
1631 /**Listener class used to veto attempts to start another app instance.
1632 * A (modal) dialog[ue] is shown instead.
1633 */
1634 private static final class SISListener implements SingleInstanceListener
1635 {
1636 public final void newActivation(final String[] params)
1637 {
1638 System.err.println("Attempted to launch another instance, args: " + Arrays.asList(params));
1639
1640 // Schedule a job for the event-dispatching thread:
1641 // creating and a blocking dialogue.
1642 SwingUtilities.invokeLater(new Runnable(){
1643 public final void run()
1644 {
1645 JOptionPane.showMessageDialog(null, "Gallery 3D Walkthrough already running...", "Already running", JOptionPane.ERROR_MESSAGE);
1646 }
1647 });
1648 }
1649 }
1650
1651 /**Main method invoked from JWS.
1652 */
1653 public static void main(final String[] args)
1654 {
1655 // Try to set a local look-and-feel...
1656 try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
1657 catch(final Exception e) { }
1658
1659 // Schedule a job for the event-dispatching thread:
1660 // creating and showing this application's GUI.
1661 // Also start a daemon thread to drive async non-GUI activity.
1662 SwingUtilities.invokeLater(new Runnable(){
1663 public final void run()
1664 {
1665 final ThreeDMain mainFrame = new ThreeDMain();
1666 mainFrame.pack();
1667 mainFrame.setVisible(true);
1668
1669 // Try to "warm up" first exhibit that user will see.
1670 mainFrame.logic.getThumbnailImageAsTexture(0, false, false);
1671 mainFrame.logic.getThumbnailImageAsTexture(0, true, false);
1672
1673 // After displaying the UI,
1674 // but before allowing much else to happen,
1675 // reload any persisted data
1676 // and start any background processing...
1677 mainFrame.logic.startup();
1678
1679 // Start a (daemon) poller thread for UI async activity.
1680 // This is called from a Swing timerUI, so is Swing-safe.
1681 // This is created and started AFTER the UI object construction
1682 // is complete so as to try to avoid any unpleasant races.
1683 // We call this every 100ms or so in order to feel responsive.
1684 final Timer timerUI = new Timer(70 + Rnd.fastRnd.nextInt(30),
1685 (new ActionListener(){
1686 /**Poll the UI logic. */
1687 public void actionPerformed(final ActionEvent e)
1688 { mainFrame.pollUI(/* e */); }
1689 }));
1690 // Delay first poll a little to allow the system to warm up...
1691 timerUI.setInitialDelay(301);
1692 timerUI.start();
1693
1694
1695 // Create and attach and animate the scene.
1696 if(mainFrame.canvas3D != null)
1697 {
1698 // Create our Universe...
1699 final SimpleUniverse u = new SimpleUniverse(mainFrame.canvas3D);
1700 mainFrame.simpleUniverse = u;
1701 final ViewingPlatform viewingPlatform = u.getViewingPlatform();
1702 final ViewPlatform vp = viewingPlatform.getViewPlatform();
1703 final Locale l = u.getLocale();
1704 l.removeBranchGraph(viewingPlatform);
1705 vp.setCapability(Node.ALLOW_LOCAL_TO_VWORLD_READ);
1706 l.addBranchGraph(viewingPlatform);
1707
1708 // Create a simple banner and attach it to the universe.
1709 final BranchGroup banner = createMainBannerSceneGraph(new Point3f());
1710 u.addBranchGraph(banner);
1711
1712 // Attach main exhibits branch to the universe.
1713 // We will add/remove visible exhibits to/from this group.
1714 final BranchGroup exhibitsBranchGroup = mainFrame.exhibitsBranchGroup;
1715 exhibitsBranchGroup.compile();
1716 u.addBranchGraph(exhibitsBranchGroup);
1717
1718 // Set up user controls of view platform...
1719 mainFrame.initUserControls(u, exhibitsBranchGroup);
1720
1721 // Try to "warm up" first exhibit that user will see.
1722 mainFrame.logic.getThumbnailImageAsTexture(0, false, false);
1723 mainFrame.logic.getThumbnailImageAsTexture(0, true, false);
1724 }
1725 }
1726 });
1727 }
1728
1729 /**Unique Serialisation class ID generated by http://random.hd.org/. */
1730 private static final long serialVersionUID = 8885229711913005041L;
1731 }