001    /*
002     * File $Workfile: CanvasPanel.java $
003     * $Archive: /Tower Of Hanoi/toh/src/net/pacbell/jfai/toh/ui/CanvasPanel.java $
004     * Last changed by $Author: Jürgen Failenschmid $
005     * Last modified   $Modtime: 12/09/05 4:57p $
006     * Last checked in $Date: 12/09/05 5:10p $
007     * $Revision: 8 $
008     *
009     *
010     * Copyright (C) 2004-2005 Jürgen Failenschmid. All rights reserved.
011     */
012    
013    package net.pacbell.jfai.toh.ui;
014    
015    import java.awt.BorderLayout;
016    import java.awt.Dimension;
017    import java.util.List;
018    import java.util.Vector;
019    
020    import javax.media.j3d.AmbientLight;
021    import javax.media.j3d.Background;
022    import javax.media.j3d.BoundingSphere;
023    import javax.media.j3d.BranchGroup;
024    import javax.media.j3d.DirectionalLight;
025    import javax.media.j3d.Group;
026    import javax.media.j3d.Light;
027    import javax.media.j3d.PointLight;
028    import javax.media.j3d.Transform3D;
029    import javax.media.j3d.TransformGroup;
030    import javax.swing.BorderFactory;
031    import javax.swing.JPanel;
032    import javax.swing.border.TitledBorder;
033    import javax.vecmath.Color3f;
034    import javax.vecmath.Point3d;
035    import javax.vecmath.Point3f;
036    import javax.vecmath.Vector3d;
037    import javax.vecmath.Vector3f;
038    
039    import com.sun.j3d.utils.behaviors.vp.OrbitBehavior;
040    import com.sun.j3d.utils.picking.PickTool;
041    import com.sun.j3d.utils.picking.behaviors.PickingCallback;
042    import com.sun.j3d.utils.universe.SimpleUniverse;
043    
044    import net.pacbell.jfai.toh.domain.Base;
045    import net.pacbell.jfai.toh.domain.Disk;
046    import net.pacbell.jfai.toh.domain.Pin;
047    import net.pacbell.jfai.toh.util.BufferedValue;
048    
049    import org.apache.commons.logging.Log;
050    import org.apache.commons.logging.LogFactory;
051    
052    /**
053     * A panel containing the 3D-view of the puzzle.
054     * 
055     * @author {@link <a href="http://www.anycpu.com">Jürgen Failenschmid</a>}
056     */
057    @SuppressWarnings("serial")
058    public class CanvasPanel extends JPanel
059    {
060    
061        /**
062         * Adjusts the viewing distance during the first paint call. The scene's
063         * lowest point is assumed at y = 0, and the scene extends by d equally
064         * around the origin along the z-axis. By default, the view platform is at
065         * (0,0,0). The view platform needs moving away so that the scene can be
066         * viewed in full. The wider the canvas, the closer the view platform can
067         * be to still see the full width of the scene. The optimal viewing
068         * distance depends on the canvas size, which is not known until the canvas
069         * is painted the first time. If the view platform is located at x = y = 0
070         * then to be able to see the virtual width (2 * w) within the current
071         * canvas width - the same distance to either side of the view platform,
072         * the view platform has to be moved on the z-axis by: zw = w / tan(FOV/2),
073         * where FOV is the field of view in radians. With the default view
074         * settings, only the canvas width is considered and the ratio between
075         * window width and height needs to be applied to the scene's height h: zhy =
076         * h * cw / ch / tan(FOV/2) If the view platform is moved along the y-axis
077         * by y measured from the lowest point of the scene then zy = [d / 2 + y /
078         * tan(FOV/2)] * cw / ch. The optimal location of the view platform is
079         * max(zw, zhy, zy).
080         */
081        private class FirstPaintListener implements Canvas3D.IPaintListener {
082            private static final double SCENE_EXTRA_Z = 0.3d;
083    
084            private static final double SCENE_Y_TRANSLATION_FACTOR = 1d;
085    
086            /**
087             * The canvas is painted the very first time. Changes the view point so
088             * that the scene is completely visible.
089             * 
090             * @see net.pacbell.jfai.toh.ui.Canvas3D.IPaintListener#firstPaintNotify(net.pacbell.jfai.toh.ui.Canvas3D)
091             */
092            public void firstPaintNotify(Canvas3D aCanvas) {
093                final double tanFov = Math
094                    .tan(aCanvas.getView().getFieldOfView() / 2);
095                final double sceneWidth = TowerOfHanoiView.width();
096                final double sceneHeight = TowerOfHanoiView.height();
097                final double sceneDepth = TowerOfHanoiView.depth();
098                final double zw = sceneWidth / 2 / tanFov;
099    
100                final Dimension canvasSize = aCanvas.getSize();
101                final double goodY = sceneHeight * SCENE_Y_TRANSLATION_FACTOR;
102                final double zy = Math.max(sceneDepth / 2 + goodY / tanFov,
103                    sceneHeight / tanFov)
104                    * canvasSize.width / canvasSize.height;
105                final double goodZ = Math.max(zw, zy) + SCENE_EXTRA_Z;
106    
107                final TransformGroup viewingPlatformTransformGroup = getUniverse()
108                    .getViewingPlatform().getViewPlatformTransform();
109                final Transform3D transform = new Transform3D();
110                viewingPlatformTransformGroup.getTransform(transform);
111                final Vector3d translation = new Vector3d();
112                transform.get(translation);
113                translation.set(translation.x, goodY, goodZ);
114                // Only set the translation component of the transform
115                transform.setTranslation(translation);
116                viewingPlatformTransformGroup.setTransform(transform);
117    
118                if (LOG.isDebugEnabled()) {
119                    LOG.debug("firstPaintNotify: tanFOV=" + tanFov //$NON-NLS-1$
120                        + ", scene width=" + sceneWidth //$NON-NLS-1$
121                        + ", scene height=" + sceneHeight //$NON-NLS-1$
122                        + ", canvas size=" + canvasSize //$NON-NLS-1$
123                        + ", zw=" + zw + ", zy=" + zy); //$NON-NLS-1$ //$NON-NLS-2$
124                    LOG.debug("    ==> z-translation = " + translation.z); //$NON-NLS-1$
125                }
126            }
127        }
128    
129        /**
130         * The trace log.
131         */
132        static final Log LOG = LogFactory.getLog(CanvasPanel.class);
133    
134        private static final float AMBIENT_LIGHT__COLOR_SATURATION = 0.3f;
135        private static final Color3f AMBIENT_LIGHT_COLOR = new Color3f(
136            AMBIENT_LIGHT__COLOR_SATURATION, AMBIENT_LIGHT__COLOR_SATURATION,
137            AMBIENT_LIGHT__COLOR_SATURATION);
138        private static final double BOUNDING__SPHERE_RADIUS = 100.0;
139        private static final BoundingSphere BOUNDING_SPHERE = new BoundingSphere(
140            new Point3d(0.0, 0.0, 0.0), BOUNDING__SPHERE_RADIUS);
141        private static final float POINT_LIGHT__ATTENUATION_LINEAR = 0.7f;
142        private static final Point3f POINT_LIGHT_ATTENUATION = new Point3f(0.0f,
143            POINT_LIGHT__ATTENUATION_LINEAR, 0.0f);
144        private BaseView baseView;
145        private Canvas3D canvas;
146        private List<DiskView> diskViews;
147        private MousePicker mousePicker;
148        private TransformGroup sceneTransformGroup;
149        private SimpleUniverse universe;
150    
151        /**
152         * Creates a panel with the 3D drawing canvas.
153         * 
154         * @param base a Base
155         * @param disks the disks
156         */
157        public CanvasPanel(Base base, List<Disk> disks) {
158            /*
159             * Need to use a layout manager that anchors the size of the canvas to
160             * the panel's border. Note that the preferred size of a Canvas3D by
161             * default is 0 by 0.
162             */
163            super(new BorderLayout());
164            setDiskViews(new Vector<DiskView>());
165            setBorder(BorderFactory.createTitledBorder(BorderFactory
166                .createLoweredBevelBorder(),
167                "Press mouse buttons and drag to change view", //$NON-NLS-1$
168                TitledBorder.CENTER, TitledBorder.ABOVE_TOP));
169    
170            setCanvas(new Canvas3D(SimpleUniverse.getPreferredConfiguration()));
171            setUniverse(new SimpleUniverse(getCanvas()));
172            add(getCanvas());
173    
174            // Create a compiled scene graph
175            final BranchGroup scene = createSceneGraph(base, disks);
176    
177            /*
178             * Register a listener to adjust the viewing distance during the first
179             * paint call
180             */
181            getCanvas().addPaintListener(new FirstPaintListener());
182    
183            // Adds mouse orbit behavior to the ViewingPlatform
184            final OrbitBehavior orbit = new OrbitBehavior(getCanvas(),
185                OrbitBehavior.REVERSE_ALL);
186            orbit.setSchedulingBounds(BOUNDING_SPHERE);
187            getUniverse().getViewingPlatform().setViewPlatformBehavior(orbit);
188            // Adds scene graph to the universe
189            getUniverse().addBranchGraph(scene);
190        }
191    
192        /**
193         * Prepares for the selection of a pin in the scene. The parameter is the
194         * editor model for the pin that is to be selected.
195         * <p>
196         * <p>
197         * <i>Assume that this method is called in the event dispatch thread, for
198         * example, when a toggle button was pressed. </i>
199         * 
200         * @param pinModel a BufferedValue for the selected pin
201         */
202        public void beginPinSelection(final BufferedValue pinModel) {
203            getMousePicker().setCallback(new PickingCallback() {
204                /**
205                 * {@inheritDoc}
206                 * <p>
207                 * MousePicker calls this for each pick attempt. If type is
208                 * PickingCallback.ROTATE, an object was selected and the second
209                 * argument is its TransformGroup.
210                 * </p>
211                 */
212                public void transformChanged(int type, TransformGroup tg) {
213                    if (LOG.isDebugEnabled()) {
214                        final StringBuffer logMessage = new StringBuffer();
215                        logMessage
216                            .append("PickingCallback: type = " + type + ", transform group = " + tg); //$NON-NLS-1$ //$NON-NLS-2$
217                        if (tg != null) {
218                            logMessage
219                                .append(tg.getUserData() == null ? " - no user data" //$NON-NLS-1$
220                                    : " - user data = " + tg.getUserData()); //$NON-NLS-1$
221                        }
222                        else {
223                            logMessage.append(" - no pick"); //$NON-NLS-1$
224                        }
225                        LOG.debug(logMessage);
226                    }
227                    if (type == PickingCallback.ROTATE
228                        && tg.getUserData() instanceof Pin) 
229                    {
230                        pickPin(pinModel, (Pin) tg.getUserData());
231                    }
232                }
233            });
234        }
235    
236        /** Ends the visualization. */
237        public void cleanUp() {
238            LOG.debug("clean up"); //$NON-NLS-1$
239            getUniverse().removeAllLocales();
240        }
241    
242        /**
243         * Ends pin selection.
244         */
245        public void endPinSelection() {
246            // Remove mouse picker callback
247            getMousePicker().setCallback(null);
248        }
249    
250        /**
251         * Gets the 3D canvas.
252         * 
253         * @return Returns the 3D canvas.
254         */
255        public Canvas3D getCanvas() {
256            return canvas;
257        }
258    
259        /**
260         * Gets the list of disk views.
261         * 
262         * @return a list of the views of disks
263         */
264        public List<DiskView> getDiskViews() {
265            return diskViews;
266        }
267    
268        /**
269         * Refreshes all disk views by removing all existing disk views and
270         * creating a view for each of the given disks.
271         * 
272         * @param disks the disks
273         */
274        public void updateDisks(List<Disk> disks) {
275            removeDisks();
276            addDisks(disks);
277        }
278    
279        /**
280         * A pin was selected in the 3D view. Changes the given buffered value.
281         * <p>
282         * <p>
283         * <i>This could be called outside of the event dispatch thread. </i>
284         * 
285         * @param pinModel a BufferedValue for the selected pin
286         * @param pin a Pin
287         */
288        protected void pickPin(BufferedValue pinModel, Pin pin) {
289            /* Has protected visibility for faster inner class access */
290            pinModel.setValue(pin);
291        }
292    
293        /**
294         * Gets the Universe.
295         * 
296         * @return the SimpleUniverse
297         */
298        SimpleUniverse getUniverse() {
299            return universe;
300        }
301    
302        /**
303         * Adds a view for each disk located at the base's source pin.
304         */
305        private void addDisks(List<Disk> disks) {
306            DiskView.setHeight(PinView.stackHeight() / disks.size());
307            DiskView diskView;
308            for (Disk disk : disks) {
309                diskView = new DiskView(disk, getBaseView().pinView(disk.getPin())
310                    .diskLocation(disk), disks.size(), getBaseView());
311                getDiskViews().add(diskView);
312                getSceneTransformGroup().addChild(diskView.getGroup());
313            }
314        }
315    
316        private Background createBackground() {
317            final Background background = new Background(new Color3f(0.0f, 0.0f,
318                0.0f));
319            background.setApplicationBounds(BOUNDING_SPHERE);
320            return background;
321        }
322    
323        private void createLights(TransformGroup sceneTransform) {
324            // Adds ambient light
325            final AmbientLight ambientLight = new AmbientLight(
326                CanvasPanel.AMBIENT_LIGHT_COLOR);
327            ambientLight.setInfluencingBounds(BOUNDING_SPHERE);
328            sceneTransform.addChild(ambientLight);
329    
330            /*
331             * Adds a white directional light positioned in the foreground on the
332             * left
333             */
334            final Color3f lightColor = new Color3f(1.0f, 1.0f, 1.0f);
335            final Transform3D light1Transform = new Transform3D();
336            final Vector3d light1Position = new Vector3d(-1.0, 0.0, 2.0);
337            light1Transform.set(light1Position);
338            sceneTransform.addChild(new TransformGroup(light1Transform));
339            Light directionalLight1;
340            final Vector3f light1Direction = new Vector3f(light1Position);
341            light1Direction.negate();
342            directionalLight1 = new DirectionalLight(lightColor, light1Direction);
343            directionalLight1.setInfluencingBounds(BOUNDING_SPHERE);
344            sceneTransform.addChild(directionalLight1);
345    
346            /*
347             * Adds a white point light positioned above in the back on the right
348             * side
349             */
350            final Transform3D light2Transform = new Transform3D();
351            final Point3f light2Position = new Point3f(1.0f, 2.0f, -1.0f);
352            light2Transform.set(new Vector3f(light2Position));
353            sceneTransform.addChild(new TransformGroup(light2Transform));
354            final Light pointLight1 = new PointLight(lightColor, light2Position,
355                CanvasPanel.POINT_LIGHT_ATTENUATION);
356            pointLight1.setInfluencingBounds(BOUNDING_SPHERE);
357            sceneTransform.addChild(pointLight1);
358        }
359    
360        private BranchGroup createSceneGraph(Base base, List<Disk> disks) {
361            // Creates the root of the branch graph
362            final BranchGroup root = new BranchGroup();
363    
364            /*
365             * Creates a TransformGroup for all objects and adds it to the scene.
366             * This transform is currently the identity transform.
367             */
368            final Transform3D sceneOrientation = new Transform3D();
369            final TransformGroup sceneTransform = new TransformGroup(
370                sceneOrientation);
371            setSceneTransformGroup(sceneTransform);
372            root.addChild(sceneTransform);
373            sceneTransform.setCapability(Group.ALLOW_CHILDREN_WRITE);
374            sceneTransform.setCapability(Group.ALLOW_CHILDREN_EXTEND);
375    
376            // Adds the background
377            sceneTransform.addChild(createBackground());
378    
379            // Adds lights
380            createLights(sceneTransform);
381    
382            // Creates the base view and adds it to the scene
383            setBaseView(new BaseView(base));
384            sceneTransform.addChild(getBaseView().getGroup());
385    
386            // Adds the disks to the scene
387            addDisks(disks);
388    
389            // Adds mouse pick behavior
390            setMousePicker(new MousePicker(root, getCanvas(), BOUNDING_SPHERE,
391                PickTool.GEOMETRY));
392            root.addChild(getMousePicker());
393    
394            root.compile();
395            return root;
396        }
397    
398        /**
399         * Gets the view of the base.
400         * 
401         * @return the view of the base
402         */
403        private BaseView getBaseView() {
404            return baseView;
405        }
406    
407        /**
408         * Gets the mouse picker.
409         * 
410         * @return a MousePicker
411         */
412        private MousePicker getMousePicker() {
413            return mousePicker;
414        }
415    
416        /**
417         * Gets the TransformGroup.
418         * 
419         * @return the TransformGroup
420         */
421        private TransformGroup getSceneTransformGroup() {
422            return sceneTransformGroup;
423        }
424    
425        /**
426         * Removes all DiskViews from the scene.
427         */
428        private void removeDisks() {
429            for (DiskView diskView : getDiskViews()) {
430                diskView.disappear();
431            }
432    
433            getDiskViews().clear();
434        }
435    
436        private void setBaseView(BaseView baseView) {
437            this.baseView = baseView;
438        }
439    
440        /**
441         * Sets the canvas.
442         * 
443         * @param canvas The canvas to set.
444         */
445        private void setCanvas(Canvas3D canvas) {
446            this.canvas = canvas;
447        }
448    
449        private void setDiskViews(List<DiskView> newDiskViews) {
450            diskViews = newDiskViews;
451        }
452    
453        private void setMousePicker(MousePicker newMousePicker) {
454            mousePicker = newMousePicker;
455        }
456    
457        private void setSceneTransformGroup(TransformGroup newSceneTransformGroup) {
458            sceneTransformGroup = newSceneTransformGroup;
459        }
460    
461        private void setUniverse(SimpleUniverse universe) {
462            this.universe = universe;
463        }
464    }