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&#46;hd&#46;org/. */
1730        private static final long serialVersionUID = 8885229711913005041L;
1731        }