1 /*
2  * ModelManager.java 4 juil. 07
3  *
4  * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 package com.eteks.sweethome3d.j3d;
21 
22 import java.awt.EventQueue;
23 import java.awt.Shape;
24 import java.awt.geom.AffineTransform;
25 import java.awt.geom.Area;
26 import java.awt.geom.GeneralPath;
27 import java.awt.geom.PathIterator;
28 import java.awt.geom.Rectangle2D;
29 import java.io.File;
30 import java.io.IOException;
31 import java.lang.reflect.Constructor;
32 import java.lang.reflect.InvocationTargetException;
33 import java.lang.reflect.Modifier;
34 import java.net.JarURLConnection;
35 import java.net.URISyntaxException;
36 import java.net.URL;
37 import java.net.URLConnection;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.Comparator;
41 import java.util.Enumeration;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.IdentityHashMap;
45 import java.util.Iterator;
46 import java.util.LinkedHashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.TreeSet;
51 import java.util.WeakHashMap;
52 import java.util.concurrent.ExecutorService;
53 import java.util.concurrent.Executors;
54 
55 import javax.media.j3d.Appearance;
56 import javax.media.j3d.BoundingBox;
57 import javax.media.j3d.Bounds;
58 import javax.media.j3d.BranchGroup;
59 import javax.media.j3d.ColoringAttributes;
60 import javax.media.j3d.Geometry;
61 import javax.media.j3d.GeometryArray;
62 import javax.media.j3d.GeometryStripArray;
63 import javax.media.j3d.Group;
64 import javax.media.j3d.IndexedGeometryArray;
65 import javax.media.j3d.IndexedGeometryStripArray;
66 import javax.media.j3d.IndexedLineArray;
67 import javax.media.j3d.IndexedLineStripArray;
68 import javax.media.j3d.IndexedQuadArray;
69 import javax.media.j3d.IndexedTriangleArray;
70 import javax.media.j3d.IndexedTriangleFanArray;
71 import javax.media.j3d.IndexedTriangleStripArray;
72 import javax.media.j3d.Light;
73 import javax.media.j3d.LineArray;
74 import javax.media.j3d.LineAttributes;
75 import javax.media.j3d.LineStripArray;
76 import javax.media.j3d.Link;
77 import javax.media.j3d.Material;
78 import javax.media.j3d.Node;
79 import javax.media.j3d.PointAttributes;
80 import javax.media.j3d.PolygonAttributes;
81 import javax.media.j3d.QuadArray;
82 import javax.media.j3d.RenderingAttributes;
83 import javax.media.j3d.Shape3D;
84 import javax.media.j3d.SharedGroup;
85 import javax.media.j3d.TexCoordGeneration;
86 import javax.media.j3d.Texture;
87 import javax.media.j3d.TextureAttributes;
88 import javax.media.j3d.Transform3D;
89 import javax.media.j3d.TransformGroup;
90 import javax.media.j3d.TransparencyAttributes;
91 import javax.media.j3d.TriangleArray;
92 import javax.media.j3d.TriangleFanArray;
93 import javax.media.j3d.TriangleStripArray;
94 import javax.vecmath.Color3f;
95 import javax.vecmath.Matrix3f;
96 import javax.vecmath.Point3d;
97 import javax.vecmath.Point3f;
98 import javax.vecmath.TexCoord2f;
99 import javax.vecmath.Vector3d;
100 import javax.vecmath.Vector3f;
101 
102 import com.eteks.sweethome3d.model.CatalogTexture;
103 import com.eteks.sweethome3d.model.Content;
104 import com.eteks.sweethome3d.model.HomeMaterial;
105 import com.eteks.sweethome3d.model.HomePieceOfFurniture;
106 import com.eteks.sweethome3d.model.HomeTexture;
107 import com.eteks.sweethome3d.model.Room;
108 import com.eteks.sweethome3d.tools.OperatingSystem;
109 import com.eteks.sweethome3d.tools.SimpleURLContent;
110 import com.eteks.sweethome3d.tools.TemporaryURLContent;
111 import com.eteks.sweethome3d.tools.URLContent;
112 import com.sun.j3d.loaders.IncorrectFormatException;
113 import com.sun.j3d.loaders.Loader;
114 import com.sun.j3d.loaders.ParsingErrorException;
115 import com.sun.j3d.loaders.Scene;
116 import com.sun.j3d.loaders.lw3d.Lw3dLoader;
117 
118 /**
119  * Singleton managing 3D models cache.
120  * This manager supports 3D models with an OBJ, DAE, 3DS or LWS format by default.
121  * Additional classes implementing Java 3D <code>Loader</code> interface may be
122  * specified in the <code>com.eteks.sweethome3d.j3d.additionalLoaderClasses</code>
123  * (separated by a space or a colon :) to enable the support of other formats.<br>
124  * Note: this class is compatible with Java 3D 1.3.
125  * @author Emmanuel Puybaret
126  */
127 public class ModelManager {
128   /**
129    * <code>Shape3D</code> user data prefix for window pane shapes.
130    */
131   public static final String    WINDOW_PANE_SHAPE_PREFIX = "sweethome3d_window_pane";
132   /**
133    * <code>Shape3D</code> user data prefix for mirror shapes.
134    */
135   public static final String    MIRROR_SHAPE_PREFIX = "sweethome3d_window_mirror";
136   /**
137    * <code>Shape3D</code> user data prefix for lights.
138    */
139   public static final String    LIGHT_SHAPE_PREFIX = "sweethome3d_light";
140   /**
141    * <code>Node</code> user data prefix for mannequin parts.
142    */
143   public static final String    MANNEQUIN_ABDOMEN_PREFIX        = "sweethome3d_mannequin_abdomen";
144   public static final String    MANNEQUIN_CHEST_PREFIX          = "sweethome3d_mannequin_chest";
145   public static final String    MANNEQUIN_PELVIS_PREFIX         = "sweethome3d_mannequin_pelvis";
146   public static final String    MANNEQUIN_NECK_PREFIX           = "sweethome3d_mannequin_neck";
147   public static final String    MANNEQUIN_HEAD_PREFIX           = "sweethome3d_mannequin_head";
148   public static final String    MANNEQUIN_LEFT_SHOULDER_PREFIX  = "sweethome3d_mannequin_left_shoulder";
149   public static final String    MANNEQUIN_LEFT_ARM_PREFIX       = "sweethome3d_mannequin_left_arm";
150   public static final String    MANNEQUIN_LEFT_ELBOW_PREFIX     = "sweethome3d_mannequin_left_elbow";
151   public static final String    MANNEQUIN_LEFT_FOREARM_PREFIX   = "sweethome3d_mannequin_left_forearm";
152   public static final String    MANNEQUIN_LEFT_WRIST_PREFIX     = "sweethome3d_mannequin_left_wrist";
153   public static final String    MANNEQUIN_LEFT_HAND_PREFIX      = "sweethome3d_mannequin_left_hand";
154   public static final String    MANNEQUIN_LEFT_HIP_PREFIX       = "sweethome3d_mannequin_left_hip";
155   public static final String    MANNEQUIN_LEFT_THIGH_PREFIX     = "sweethome3d_mannequin_left_thigh";
156   public static final String    MANNEQUIN_LEFT_KNEE_PREFIX      = "sweethome3d_mannequin_left_knee";
157   public static final String    MANNEQUIN_LEFT_LEG_PREFIX       = "sweethome3d_mannequin_left_leg";
158   public static final String    MANNEQUIN_LEFT_ANKLE_PREFIX     = "sweethome3d_mannequin_left_ankle";
159   public static final String    MANNEQUIN_LEFT_FOOT_PREFIX      = "sweethome3d_mannequin_left_foot";
160   public static final String    MANNEQUIN_RIGHT_SHOULDER_PREFIX = "sweethome3d_mannequin_right_shoulder";
161   public static final String    MANNEQUIN_RIGHT_ARM_PREFIX      = "sweethome3d_mannequin_right_arm";
162   public static final String    MANNEQUIN_RIGHT_ELBOW_PREFIX    = "sweethome3d_mannequin_right_elbow";
163   public static final String    MANNEQUIN_RIGHT_FOREARM_PREFIX  = "sweethome3d_mannequin_right_forearm";
164   public static final String    MANNEQUIN_RIGHT_WRIST_PREFIX    = "sweethome3d_mannequin_right_wrist";
165   public static final String    MANNEQUIN_RIGHT_HAND_PREFIX     = "sweethome3d_mannequin_right_hand";
166   public static final String    MANNEQUIN_RIGHT_HIP_PREFIX      = "sweethome3d_mannequin_right_hip";
167   public static final String    MANNEQUIN_RIGHT_THIGH_PREFIX    = "sweethome3d_mannequin_right_thigh";
168   public static final String    MANNEQUIN_RIGHT_KNEE_PREFIX     = "sweethome3d_mannequin_right_knee";
169   public static final String    MANNEQUIN_RIGHT_LEG_PREFIX      = "sweethome3d_mannequin_right_leg";
170   public static final String    MANNEQUIN_RIGHT_ANKLE_PREFIX    = "sweethome3d_mannequin_right_ankle";
171   public static final String    MANNEQUIN_RIGHT_FOOT_PREFIX     = "sweethome3d_mannequin_right_foot";
172 
173   public static final String    MANNEQUIN_ABDOMEN_CHEST_PREFIX  = "sweethome3d_mannequin_abdomen_chest";
174   public static final String    MANNEQUIN_ABDOMEN_PELVIS_PREFIX = "sweethome3d_mannequin_abdomen_pelvis";
175   /**
176    * <code>Node</code> user data prefix for ball / rotating  joints.
177    */
178   public static final String    BALL_PREFIX                 = "sweethome3d_ball_";
179   public static final String    ARM_ON_BALL_PREFIX          = "sweethome3d_arm_on_ball_";
180   /**
181    * <code>Node</code> user data prefix for hinge / rotating opening joints.
182    */
183   public static final String    HINGE_PREFIX                = "sweethome3d_hinge_";
184   public static final String    OPENING_ON_HINGE_PREFIX     = "sweethome3d_opening_on_hinge_";
185   public static final String    WINDOW_PANE_ON_HINGE_PREFIX = WINDOW_PANE_SHAPE_PREFIX + "_on_hinge_";
186   public static final String    MIRROR_ON_HINGE_PREFIX      = MIRROR_SHAPE_PREFIX + "_on_hinge_";
187   /**
188    * <code>Node</code> user data prefix for rail / sliding opening joints.
189    */
190   public static final String    UNIQUE_RAIL_PREFIX          = "sweethome3d_unique_rail";
191   public static final String    RAIL_PREFIX                 = "sweethome3d_rail_";
192   public static final String    OPENING_ON_RAIL_PREFIX      = "sweethome3d_opening_on_rail_";
193   public static final String    WINDOW_PANE_ON_RAIL_PREFIX  = WINDOW_PANE_SHAPE_PREFIX + "_on_rail_";
194   public static final String    MIRROR_ON_RAIL_PREFIX       = MIRROR_SHAPE_PREFIX + "_on_rail_";
195   /**
196    * Deformable group suffix.
197    */
198   public static final String    DEFORMABLE_TRANSFORM_GROUP_SUFFIX = "_transformation";
199 
200   private static final TransparencyAttributes WINDOW_PANE_TRANSPARENCY_ATTRIBUTES =
201       new TransparencyAttributes(TransparencyAttributes.NICEST, 0.5f);
202 
203   private static final Material DEFAULT_MATERIAL = new Material();
204 
205   private static final float    MINIMUM_SIZE = 0.001f;
206 
207   private static final String   ADDITIONAL_LOADER_CLASSES = "com.eteks.sweethome3d.j3d.additionalLoaderClasses";
208 
209   private static ModelManager instance;
210 
211   // Map storing loaded model nodes
212   private Map<Content, BranchGroup> loadedModelNodes;
213   // Map storing model nodes being loaded
214   private Map<Content, List<ModelObserver>> loadingModelObservers;
215   // Map storing the bounds of transformed model nodes
216   private Map<Content, Map<Transform3D, BoundingBox>> transformedModelNodeBounds;
217   // Executor used to load models
218   private ExecutorService           modelsLoader;
219   // List of additional loader classes
220   private Class<Loader> []          additionalLoaderClasses;
221 
ModelManager()222   private ModelManager() {
223     // This class is a singleton
224     this.loadedModelNodes = new WeakHashMap<Content, BranchGroup>();
225     this.loadingModelObservers = new HashMap<Content, List<ModelObserver>>();
226     this.transformedModelNodeBounds = new WeakHashMap<Content, Map<Transform3D, BoundingBox>>();
227     // Load other optional Loader classes
228     List<Class<Loader>> loaderClasses = new ArrayList<Class<Loader>>();
229     String loaderClassNames = System.getProperty(ADDITIONAL_LOADER_CLASSES);
230     if (loaderClassNames != null) {
231       for (String loaderClassName : loaderClassNames.split("\\s|:")) {
232         try {
233           loaderClasses.add(getLoaderClass(loaderClassName));
234         } catch (IllegalArgumentException ex) {
235           System.err.println("Invalid loader class " + loaderClassName + ":\n" + ex.getMessage());
236         }
237       }
238     }
239     this.additionalLoaderClasses = loaderClasses.toArray(new Class [loaderClasses.size()]);
240   }
241 
242   /**
243    * Returns the class of name <code>loaderClassName</code>.
244    */
245   @SuppressWarnings("unchecked")
getLoaderClass(String loaderClassName)246   private Class<Loader> getLoaderClass(String loaderClassName) {
247     try {
248       Class<Loader> loaderClass = (Class<Loader>)getClass().getClassLoader().loadClass(loaderClassName);
249       if (!Loader.class.isAssignableFrom(loaderClass)) {
250         throw new IllegalArgumentException(loaderClassName + " not a subclass of " + Loader.class.getName());
251       } else if (Modifier.isAbstract(loaderClass.getModifiers()) || !Modifier.isPublic(loaderClass.getModifiers())) {
252         throw new IllegalArgumentException(loaderClassName + " not a public static class");
253       }
254       Constructor<Loader> constructor = loaderClass.getConstructor(new Class [0]);
255       // Try to instantiate it now to see if it won't cause any problem
256       constructor.newInstance(new Object [0]);
257       return loaderClass;
258     } catch (ClassNotFoundException ex) {
259       throw new IllegalArgumentException(ex.getMessage(), ex);
260     } catch (NoSuchMethodException ex) {
261       throw new IllegalArgumentException(ex.getMessage(), ex);
262     } catch (InvocationTargetException ex) {
263       throw new IllegalArgumentException(ex.getMessage(), ex);
264     } catch (IllegalAccessException ex) {
265       throw new IllegalArgumentException(loaderClassName + " constructor not accessible");
266     } catch (InstantiationException ex) {
267       throw new IllegalArgumentException(loaderClassName + " not a public static class");
268     }
269   }
270 
271   /**
272    * Returns an instance of this singleton.
273    */
getInstance()274   public static ModelManager getInstance() {
275     if (instance == null) {
276       instance = new ModelManager();
277     }
278     return instance;
279   }
280 
281   /**
282    * Shutdowns the multithreaded service that load models and clears loaded models cache.
283    */
clear()284   public void clear() {
285     if (this.modelsLoader != null) {
286       this.modelsLoader.shutdownNow();
287       this.modelsLoader = null;
288     }
289     synchronized (this.loadedModelNodes) {
290       this.loadedModelNodes.clear();
291     }
292     this.loadingModelObservers.clear();
293   }
294 
295   /**
296    * Returns the minimum size of a model.
297    */
getMinimumSize()298   float getMinimumSize() {
299     return MINIMUM_SIZE;
300   }
301 
302   /**
303    * Returns the size of 3D shapes of <code>node</code>.
304    * This method computes the exact box that contains all the shapes,
305    * contrary to <code>node.getBounds()</code> that returns a bounding
306    * sphere for a scene.
307    * @param node     the root of a model
308    */
getSize(Node node)309   public Vector3f getSize(Node node) {
310     return getSize(node, new Transform3D());
311   }
312 
313   /**
314    * Returns the size of 3D shapes of <code>node</code> after an additional <code>transformation</code>.
315    * This method computes the exact box that contains all the shapes,
316    * contrary to <code>node.getBounds()</code> that returns a bounding
317    * sphere for a scene.
318    * @param node     the root of a model
319    * @param transformation the transformation applied to the model
320    *                 or <code>null</code> if no transformation should be applied to node.
321    */
getSize(Node node, Transform3D transformation)322   public Vector3f getSize(Node node, Transform3D transformation) {
323     BoundingBox bounds = getBounds(node, transformation);
324     Point3d lower = new Point3d();
325     bounds.getLower(lower);
326     Point3d upper = new Point3d();
327     bounds.getUpper(upper);
328     return new Vector3f(Math.max(getMinimumSize(), (float)(upper.x - lower.x)),
329         Math.max(getMinimumSize(), (float)(upper.y - lower.y)),
330         Math.max(getMinimumSize(), (float)(upper.z - lower.z)));
331   }
332 
333   /**
334    * Returns the center of the bounds of <code>node</code> 3D shapes.
335    * @param node  the root of a model
336    */
getCenter(Node node)337   public Point3f getCenter(Node node) {
338     BoundingBox bounds = getBounds(node);
339     Point3d lower = new Point3d();
340     bounds.getLower(lower);
341     Point3d upper = new Point3d();
342     bounds.getUpper(upper);
343     return new Point3f((float)(lower.x + upper.x) / 2,
344         (float)(lower.y + upper.y) / 2,
345         (float)(lower.z + upper.z) / 2);
346   }
347 
348   /**
349    * Returns the bounds of the 3D shapes of <code>node</code>.
350    * This method computes the exact box that contains all the shapes,
351    * contrary to <code>node.getBounds()</code> that returns a bounding
352    * sphere for a scene.
353    * @param node  the root of a model
354    */
getBounds(Node node)355   public BoundingBox getBounds(Node node) {
356     return getBounds(node, new Transform3D());
357   }
358 
359   /**
360    * Returns the bounds of the 3D shapes of <code>node</code> with an additional <code>transformation</code>.
361    * This method computes the exact box that contains all the shapes, contrary to <code>node.getBounds()</code>
362    * that returns a bounding sphere for a scene.
363    * @param node     the root of a model
364    * @param transformation the transformation applied to the model
365    *                 or <code>null</code> if no transformation should be applied to node.
366    */
getBounds(Node node, Transform3D transformation)367   public BoundingBox getBounds(Node node, Transform3D transformation) {
368     BoundingBox objectBounds = new BoundingBox(
369         new Point3d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY),
370         new Point3d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY));
371     computeBounds(node, objectBounds, transformation, !isOrthogonalRotation(transformation), isDeformed(node));
372     Point3d lower = new Point3d();
373     objectBounds.getLower(lower);
374     if (lower.x == Double.POSITIVE_INFINITY) {
375       throw new IllegalArgumentException("Node has no bounds");
376     }
377     return objectBounds;
378   }
379 
380   /**
381    * Returns <code>true</code> if the rotation matrix matches only rotations of
382    * a multiple of 90� degrees around x, y or z axis.
383    */
isOrthogonalRotation(Transform3D transformation)384   private boolean isOrthogonalRotation(Transform3D transformation) {
385     Matrix3f matrix = new Matrix3f();
386     transformation.get(matrix);
387     for (int i = 0; i < 3; i++) {
388       for (int j = 0; j < 3; j++) {
389         // Return false if the matrix contains a value different from 0 1 or -1
390         if (Math.abs(matrix.getElement(i, j)) > 1E-6
391             && Math.abs(matrix.getElement(i, j) - 1) > 1E-6
392             && Math.abs(matrix.getElement(i, j) + 1) > 1E-6) {
393           return false;
394         }
395       }
396     }
397     return true;
398   }
399 
computeBounds(Node node, BoundingBox bounds, Transform3D parentTransformation, boolean transformShapeGeometry, boolean deformedGeometry)400   private void computeBounds(Node node, BoundingBox bounds, Transform3D parentTransformation,
401                              boolean transformShapeGeometry, boolean deformedGeometry) {
402     if (node instanceof Group) {
403       Map<Transform3D, BoundingBox> modelBounds = null;
404       BoundingBox transformationModelBounds = null;
405       if (node instanceof TransformGroup) {
406         parentTransformation = new Transform3D(parentTransformation);
407         Transform3D transform = new Transform3D();
408         ((TransformGroup)node).getTransform(transform);
409         parentTransformation.mul(transform);
410       } else if (transformShapeGeometry
411                  && !deformedGeometry
412                  && node instanceof BranchGroup
413                  && node.getUserData() instanceof Content) {
414         // Check if it's the node of a model
415         modelBounds = this.transformedModelNodeBounds.get(node.getUserData());
416         if (modelBounds != null) {
417           // Retrieve the bounds that may have been previously computed for the requested transformation
418           transformationModelBounds = modelBounds.get(parentTransformation);
419         }
420       }
421 
422       if (transformationModelBounds == null) {
423         BoundingBox combinedBounds;
424         if (modelBounds != null) {
425           combinedBounds = new BoundingBox(
426               new Point3d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY),
427               new Point3d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY));
428          } else {
429            combinedBounds = bounds;
430          }
431 
432         // Compute the bounds of all the node children
433         Enumeration<?> enumeration = ((Group)node).getAllChildren();
434         while (enumeration.hasMoreElements ()) {
435           computeBounds((Node)enumeration.nextElement(), combinedBounds, parentTransformation,
436               transformShapeGeometry, deformedGeometry);
437         }
438 
439         if (modelBounds != null) {
440           // Store the computed bounds of the model
441           modelBounds.put(parentTransformation, transformationModelBounds = combinedBounds);
442         }
443       }
444 
445       if (transformationModelBounds != null) {
446         bounds.combine(transformationModelBounds);
447       }
448     } else if (node instanceof Link) {
449       computeBounds(((Link)node).getSharedGroup(), bounds, parentTransformation,
450           transformShapeGeometry, deformedGeometry);
451     } else if (node instanceof Shape3D) {
452       Shape3D shape = (Shape3D)node;
453       Bounds shapeBounds;
454       if (transformShapeGeometry
455           || deformedGeometry
456              && !isOrthogonalRotation(parentTransformation)) {
457         shapeBounds = computeTransformedGeometryBounds(shape, parentTransformation);
458       } else {
459         shapeBounds = shape.getBounds();
460         shapeBounds.transform(parentTransformation);
461       }
462       bounds.combine(shapeBounds);
463     }
464   }
465 
computeTransformedGeometryBounds(Shape3D shape, Transform3D transformation)466   private Bounds computeTransformedGeometryBounds(Shape3D shape, Transform3D transformation) {
467     Point3d lower = new Point3d(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
468     Point3d upper = new Point3d(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
469     for (int i = 0, n = shape.numGeometries(); i < n; i++) {
470       Geometry geometry = shape.getGeometry(i);
471       if (geometry instanceof GeometryArray) {
472         GeometryArray geometryArray = (GeometryArray)geometry;
473         int vertexCount = geometryArray.getVertexCount();
474         Point3f vertex = new Point3f();
475         if ((geometryArray.getVertexFormat() & GeometryArray.BY_REFERENCE) != 0) {
476           if ((geometryArray.getVertexFormat() & GeometryArray.INTERLEAVED) != 0) {
477             float [] vertexData = geometryArray.getInterleavedVertices();
478             int vertexSize = vertexData.length / vertexCount;
479             for (int index = 0, j = vertexSize - 3; index < vertexCount; j += vertexSize, index++) {
480               vertex.x = vertexData [j];
481               vertex.y = vertexData [j + 1];
482               vertex.z = vertexData [j + 2];
483               updateBounds(vertex, transformation, lower, upper);
484             }
485           } else {
486             float [] vertexCoordinates = geometryArray.getCoordRefFloat();
487             for (int index = 0, j = 0; index < vertexCount; j += 3, index++) {
488               vertex.x = vertexCoordinates [j];
489               vertex.y = vertexCoordinates [j + 1];
490               vertex.z = vertexCoordinates [j + 2];
491               updateBounds(vertex, transformation, lower, upper);
492             }
493           }
494         } else {
495           for (int index = 0; index < vertexCount; index++) {
496             geometryArray.getCoordinate(index, vertex);
497             updateBounds(vertex, transformation, lower, upper);
498           }
499         }
500       } else {
501         Bounds shapeBounds = shape.getBounds();
502         shapeBounds.transform(transformation);
503         return shapeBounds;
504       }
505     }
506     Bounds shapeBounds = new BoundingBox(lower, upper);
507     return shapeBounds;
508   }
509 
updateBounds(Point3f vertex, Transform3D transformation, Point3d lower, Point3d upper)510   private void updateBounds(Point3f vertex, Transform3D transformation, Point3d lower, Point3d upper) {
511     transformation.transform(vertex);
512     if (lower.x > vertex.x) {
513       lower.x = vertex.x;
514     }
515     if (lower.y > vertex.y) {
516       lower.y = vertex.y;
517     }
518     if (lower.z > vertex.z) {
519       lower.z = vertex.z;
520     }
521     if (upper.x < vertex.x) {
522       upper.x = vertex.x;
523     }
524     if (upper.y < vertex.y) {
525       upper.y = vertex.y;
526     }
527     if (upper.z < vertex.z) {
528       upper.z = vertex.z;
529     }
530   }
531 
532   /**
533    * Returns a transform group that will transform the model <code>node</code>
534    * to let it fill a box of the given <code>width</code> centered on the origin.
535    * @param node     the root of a model with any size and location
536    * @param modelRotation the rotation applied to the model before normalization
537    *                 or <code>null</code> if no transformation should be applied to node
538    * @param width    the width of the box
539    */
getNormalizedTransformGroup(Node node, float [][] modelRotation, float width)540   public TransformGroup getNormalizedTransformGroup(Node node, float [][] modelRotation, float width) {
541     return new TransformGroup(getNormalizedTransform(node, modelRotation, width, true));
542   }
543 
544   /**
545    * Returns a transform group that will transform the model <code>node</code>
546    * to let it fill a box of the given <code>width</code> centered on the origin.
547    * @param node     the root of a model with any size and location
548    * @param modelRotation the rotation applied to the model before normalization
549    *                 or <code>null</code> if no transformation should be applied to node
550    * @param width    the width of the box
551    * @param modelCenteredAtOrigin if <code>true</code> center will be moved to match the origin
552    *                 after the model rotation is applied
553    */
getNormalizedTransformGroup(Node node, float [][] modelRotation, float width, boolean modelCenteredAtOrigin)554   public TransformGroup getNormalizedTransformGroup(Node node, float [][] modelRotation, float width,
555                                                     boolean modelCenteredAtOrigin) {
556     return new TransformGroup(getNormalizedTransform(node, modelRotation, width, modelCenteredAtOrigin));
557   }
558 
559   /**
560    * Returns a transform that will transform the model <code>node</code>
561    * to let it fill a box of the given <code>width</code> centered on the origin.
562    * @param node     the root of a model with any size and location
563    * @param modelRotation the rotation applied to the model before normalization
564    *                 or <code>null</code> if no transformation should be applied to node
565    * @param width    the width of the box
566    */
getNormalizedTransform(Node node, float [][] modelRotation, float width)567   public Transform3D getNormalizedTransform(Node node, float [][] modelRotation, float width) {
568     return getNormalizedTransform(node, modelRotation, width, true);
569   }
570 
571  /**
572    * Returns a transform that will transform the model <code>node</code>
573    * to let it fill a box of the given <code>width</code> centered on the origin.
574    * @param node     the root of a model with any size and location
575    * @param modelRotation the rotation applied to the model before normalization
576    *                 or <code>null</code> if no transformation should be applied to node
577    * @param width    the width of the box
578    * @param modelCenteredAtOrigin if <code>true</code> center will be moved to match the origin
579    *                 after the model rotation is applied
580    */
getNormalizedTransform(Node node, float [][] modelRotation, float width, boolean modelCenteredAtOrigin)581   public Transform3D getNormalizedTransform(Node node, float [][] modelRotation, float width,
582                                             boolean modelCenteredAtOrigin) {
583     // Get model bounding box size
584     BoundingBox modelBounds = getBounds(node);
585     Point3d lower = new Point3d();
586     modelBounds.getLower(lower);
587     Point3d upper = new Point3d();
588     modelBounds.getUpper(upper);
589     // Translate model to its center
590     Transform3D translation = new Transform3D();
591     translation.setTranslation(new Vector3d(
592         -lower.x - (upper.x - lower.x) / 2,
593         -lower.y - (upper.y - lower.y) / 2,
594         -lower.z - (upper.z - lower.z) / 2));
595 
596     Transform3D modelTransform;
597     if (modelRotation != null) {
598       // Get model bounding box size with model rotation
599       Transform3D rotationTransform = getRotationTransformation(modelRotation);
600       rotationTransform.mul(translation);
601       BoundingBox rotatedModelBounds = getBounds(node, rotationTransform);
602       rotatedModelBounds.getLower(lower);
603       rotatedModelBounds.getUpper(upper);
604       modelTransform = new Transform3D();
605       if (modelCenteredAtOrigin) {
606         // Move model back to its new center
607         modelTransform.setTranslation(new Vector3d(
608             -lower.x - (upper.x - lower.x) / 2,
609             -lower.y - (upper.y - lower.y) / 2,
610             -lower.z - (upper.z - lower.z) / 2));
611       }
612       modelTransform.mul(rotationTransform);
613     } else {
614       modelTransform = translation;
615     }
616 
617     // Scale model to make it fill a 1 unit wide box
618     Transform3D scaleOneTransform = new Transform3D();
619     scaleOneTransform.setScale (
620         new Vector3d(width / Math.max(getMinimumSize(), upper.x -lower.x),
621             width / Math.max(getMinimumSize(), upper.y - lower.y),
622             width / Math.max(getMinimumSize(), upper.z - lower.z)));
623     scaleOneTransform.mul(modelTransform);
624     return scaleOneTransform;
625   }
626 
627   /**
628    * Returns a transformation matching the given rotation.
629    */
getRotationTransformation(float [][] modelRotation)630   Transform3D getRotationTransformation(float [][] modelRotation) {
631     Matrix3f modelRotationMatrix = new Matrix3f(modelRotation [0][0], modelRotation [0][1], modelRotation [0][2],
632         modelRotation [1][0], modelRotation [1][1], modelRotation [1][2],
633         modelRotation [2][0], modelRotation [2][1], modelRotation [2][2]);
634     Transform3D modelTransform = new Transform3D();
635     modelTransform.setRotation(modelRotationMatrix);
636     return modelTransform;
637   }
638 
639   /**
640    * Returns a transformation able to place in the scene the normalized model
641    * of the given <code>piece</code>.
642    * @param piece a piece of furniture
643    * @param normalizedModelNode the node matching the normalized model of the piece.
644    *            This parameter is required only if the piece is rotated horizontally.
645    */
getPieceOfFurnitureNormalizedModelTransformation(HomePieceOfFurniture piece, Node normalizedModelNode)646   Transform3D getPieceOfFurnitureNormalizedModelTransformation(HomePieceOfFurniture piece, Node normalizedModelNode) {
647     // Set piece size
648     Transform3D scale = new Transform3D();
649     float pieceWidth = piece.getWidth();
650     // If piece model is mirrored, inverse its width
651     if (piece.isModelMirrored()) {
652       pieceWidth *= -1;
653     }
654     scale.setScale(new Vector3d(pieceWidth, piece.getHeight(), piece.getDepth()));
655 
656     Transform3D modelTransform;
657     float height;
658     if (piece.isHorizontallyRotated() && normalizedModelNode != null) {
659       Transform3D horizontalRotationAndScale = new Transform3D();
660       // Change its angles around horizontal axes
661       if (piece.getPitch() != 0) {
662         horizontalRotationAndScale.rotX(-piece.getPitch());
663       }
664       if (piece.getRoll() != 0) {
665         Transform3D rollRotation = new Transform3D();
666         rollRotation.rotZ(-piece.getRoll());
667         horizontalRotationAndScale.mul(rollRotation, horizontalRotationAndScale);
668       }
669       horizontalRotationAndScale.mul(scale);
670 
671       // Compute center location when the piece is rotated around horizontal axes
672       BoundingBox rotatedModelBounds = getBounds(normalizedModelNode, horizontalRotationAndScale);
673       Point3d lower = new Point3d();
674       rotatedModelBounds.getLower(lower);
675       Point3d upper = new Point3d();
676       rotatedModelBounds.getUpper(upper);
677       modelTransform = new Transform3D();
678       modelTransform.setTranslation(new Vector3d(
679           -lower.x - (upper.x - lower.x) / 2,
680           -lower.y - (upper.y - lower.y) / 2,
681           -lower.z - (upper.z - lower.z) / 2));
682       modelTransform.mul(horizontalRotationAndScale);
683       height = (float)Math.max(getMinimumSize(), upper.y - lower.y);
684     } else {
685       modelTransform = scale;
686       height = piece.getHeight();
687     }
688 
689     // Change its angle around vertical axis
690     Transform3D verticalRotation = new Transform3D();
691     verticalRotation.rotY(-piece.getAngle());
692     verticalRotation.mul(modelTransform);
693 
694     // Translate it to its location
695     Transform3D pieceTransform = new Transform3D();
696     float levelElevation;
697     if (piece.getLevel() != null) {
698       levelElevation = piece.getLevel().getElevation();
699     } else {
700       levelElevation = 0;
701     }
702     pieceTransform.setTranslation(new Vector3f(
703         piece.getX(),
704         piece.getElevation() + height / 2 + levelElevation,
705         piece.getY()));
706     pieceTransform.mul(verticalRotation);
707     return pieceTransform;
708   }
709 
710   /**
711    * Reads asynchronously a 3D node from <code>content</code> with supported loaders
712    * and notifies the loaded model to the given <code>modelObserver</code> once available.
713    * @param content an object containing a model
714    * @param modelObserver the observer that will be notified once the model is available
715    *    or if an error happens
716    * @throws IllegalStateException if the current thread isn't the Event Dispatch Thread.
717    */
loadModel(Content content, ModelObserver modelObserver)718   public void loadModel(Content content,
719                         ModelObserver modelObserver) {
720     loadModel(content, false, modelObserver);
721   }
722 
723   /**
724    * Reads a 3D node from <code>content</code> with supported loaders
725    * and notifies the loaded model to the given <code>modelObserver</code> once available.
726    * @param content an object containing a model
727    * @param synchronous if <code>true</code>, this method will return only once model content is loaded
728    * @param modelObserver the observer that will be notified once the model is available
729    *    or if an error happens. When the model is loaded synchronously, the observer will be notified
730    *    in the same thread as the caller, otherwise the observer will be notified in the Event
731    *    Dispatch Thread and this method must be called in Event Dispatch Thread too.
732    * @throws IllegalStateException if synchronous is <code>false</code> and the current thread isn't
733    *    the Event Dispatch Thread.
734    */
loadModel(final Content content, boolean synchronous, ModelObserver modelObserver)735   public void loadModel(final Content content,
736                         boolean synchronous,
737                         ModelObserver modelObserver) {
738     BranchGroup modelRoot;
739     synchronized (this.loadedModelNodes) {
740       modelRoot = this.loadedModelNodes.get(content);
741     }
742     if (modelRoot != null) {
743       // Notify cached model to observer with a clone of the model
744       modelObserver.modelUpdated((BranchGroup)cloneNode(modelRoot));
745     } else if (synchronous) {
746       try {
747         modelRoot = loadModel(content);
748         synchronized (this.loadedModelNodes) {
749           // Store in cache model node for future copies
750           this.loadedModelNodes.put(content, (BranchGroup)modelRoot);
751           this.transformedModelNodeBounds.put(content, new WeakHashMap<Transform3D, BoundingBox>());
752         }
753         modelObserver.modelUpdated((BranchGroup)cloneNode(modelRoot));
754       } catch (IOException ex) {
755         modelObserver.modelError(ex);
756       }
757     } else if (!EventQueue.isDispatchThread()) {
758       throw new IllegalStateException("Asynchronous call out of Event Dispatch Thread");
759     } else {
760       if (this.modelsLoader == null) {
761         this.modelsLoader = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
762       }
763       List<ModelObserver> observers = this.loadingModelObservers.get(content);
764       if (observers != null) {
765         // If observers list exists, content model is already being loaded
766         // register observer for future notification
767         observers.add(modelObserver);
768       } else {
769         // Create a list of observers that will be notified once content model is loaded
770         observers = new ArrayList<ModelObserver>();
771         observers.add(modelObserver);
772         this.loadingModelObservers.put(content, observers);
773 
774         // Load the model in an other thread
775         this.modelsLoader.execute(new Runnable() {
776           public void run() {
777             try {
778               final BranchGroup loadedModel = loadModel(content);
779               synchronized (loadedModelNodes) {
780                 // Update loaded models cache and notify registered observers
781                 loadedModelNodes.put(content, loadedModel);
782                 transformedModelNodeBounds.put(content, new WeakHashMap<Transform3D, BoundingBox>());
783               }
784               EventQueue.invokeLater(new Runnable() {
785                   public void run() {
786                     List<ModelObserver> observers = loadingModelObservers.remove(content);
787                     if (observers != null) {
788                       for (final ModelObserver observer : observers) {
789                         observer.modelUpdated((BranchGroup)cloneNode(loadedModel));
790                       }
791                     }
792                   }
793                 });
794             } catch (final IOException ex) {
795               EventQueue.invokeLater(new Runnable() {
796                   public void run() {
797                     List<ModelObserver> observers = loadingModelObservers.remove(content);
798                     if (observers != null) {
799                       for (final ModelObserver observer : observers) {
800                         observer.modelError(ex);
801                       }
802                     }
803                   }
804                 });
805             }
806           }
807         });
808       }
809     }
810   }
811 
812   /**
813    * Returns a clone of the given <code>node</code>.
814    * All the children and the attributes of the given node are duplicated except the geometries
815    * and the texture images of shapes.
816    */
cloneNode(Node node)817   public Node cloneNode(Node node) {
818     // Clone node in a synchronized block because cloneNodeComponent is not thread safe
819     synchronized (this.loadedModelNodes) {
820       return cloneNode(node, new HashMap<SharedGroup, SharedGroup>());
821     }
822   }
823 
cloneNode(Node node, Map<SharedGroup, SharedGroup> clonedSharedGroups)824   private Node cloneNode(Node node, Map<SharedGroup, SharedGroup> clonedSharedGroups) {
825     if (node instanceof Shape3D) {
826       Shape3D shape = (Shape3D)node;
827       Shape3D clonedShape = (Shape3D)shape.cloneNode(false);
828       Appearance appearance = shape.getAppearance();
829       if (appearance != null) {
830         // Duplicate node's appearance except its texture
831         Appearance clonedAppearance = (Appearance)appearance.cloneNodeComponent(false);
832         Material material = appearance.getMaterial();
833         if (material != null) {
834           clonedAppearance.setMaterial((Material)material.cloneNodeComponent(true));
835         }
836         ColoringAttributes coloringAttributes = appearance.getColoringAttributes();
837         if (coloringAttributes != null) {
838           clonedAppearance.setColoringAttributes((ColoringAttributes)coloringAttributes.cloneNodeComponent(true));
839         }
840         TransparencyAttributes transparencyAttributes = appearance.getTransparencyAttributes();
841         if (transparencyAttributes != null) {
842           clonedAppearance.setTransparencyAttributes((TransparencyAttributes)transparencyAttributes.cloneNodeComponent(true));
843         }
844         RenderingAttributes renderingAttributes = appearance.getRenderingAttributes();
845         if (renderingAttributes != null) {
846           clonedAppearance.setRenderingAttributes((RenderingAttributes)renderingAttributes.cloneNodeComponent(true));
847         }
848         PolygonAttributes polygonAttributes = appearance.getPolygonAttributes();
849         if (polygonAttributes != null) {
850           clonedAppearance.setPolygonAttributes((PolygonAttributes)polygonAttributes.cloneNodeComponent(true));
851         }
852         LineAttributes lineAttributes = appearance.getLineAttributes();
853         if (lineAttributes != null) {
854           clonedAppearance.setLineAttributes((LineAttributes)lineAttributes.cloneNodeComponent(true));
855         }
856         PointAttributes pointAttributes = appearance.getPointAttributes();
857         if (pointAttributes != null) {
858           clonedAppearance.setPointAttributes((PointAttributes)pointAttributes.cloneNodeComponent(true));
859         }
860         TextureAttributes textureAttributes = appearance.getTextureAttributes();
861         if (textureAttributes != null) {
862           clonedAppearance.setTextureAttributes((TextureAttributes)textureAttributes.cloneNodeComponent(true));
863         }
864         TexCoordGeneration texCoordGeneration = appearance.getTexCoordGeneration();
865         if (texCoordGeneration != null) {
866           clonedAppearance.setTexCoordGeneration((TexCoordGeneration)texCoordGeneration.cloneNodeComponent(true));
867         }
868 
869         clonedShape.setAppearance(clonedAppearance);
870       }
871       return clonedShape;
872     } else if (node instanceof Link) {
873       Link clonedLink = (Link)node.cloneNode(true);
874       // Force duplication of shared groups too
875       SharedGroup sharedGroup = clonedLink.getSharedGroup();
876       if (sharedGroup != null) {
877         SharedGroup clonedSharedGroup = clonedSharedGroups.get(sharedGroup);
878         if (clonedSharedGroup == null) {
879           clonedSharedGroup = (SharedGroup)cloneNode(sharedGroup, clonedSharedGroups);
880           clonedSharedGroups.put(sharedGroup, clonedSharedGroup);
881         }
882         clonedLink.setSharedGroup(clonedSharedGroup);
883       }
884       return clonedLink;
885     } else {
886       Node clonedNode = node.cloneNode(true);
887       if (node instanceof Group) {
888         Group group = (Group)node;
889         Group clonedGroup = (Group)clonedNode;
890         for (int i = 0, n = group.numChildren(); i < n; i++) {
891           Node clonedChild = cloneNode(group.getChild(i), clonedSharedGroups);
892           clonedGroup.addChild(clonedChild);
893         }
894       }
895       return clonedNode;
896     }
897   }
898 
899   /**
900    * Returns the node loaded synchronously from <code>content</code> with supported loaders.
901    * This method is threadsafe and may be called from any thread.
902    * @param content an object containing a model
903    */
loadModel(Content content)904   public BranchGroup loadModel(Content content) throws IOException {
905     // Ensure we use a URLContent object
906     URLContent urlContent;
907     if (content instanceof URLContent) {
908       urlContent = (URLContent)content;
909     } else {
910       urlContent = TemporaryURLContent.copyToTemporaryURLContent(content);
911     }
912     Loader []  defaultLoaders = new Loader [] {new OBJLoader(),
913                                                new DAELoader(),
914                                                new Max3DSLoader(),
915                                                new Lw3dLoader()};
916     Loader [] loaders = new Loader [defaultLoaders.length + this.additionalLoaderClasses.length];
917     System.arraycopy(defaultLoaders, 0, loaders, 0, defaultLoaders.length);
918     for (int i = 0; i < this.additionalLoaderClasses.length; i++) {
919       try {
920         loaders [defaultLoaders.length + i] = this.additionalLoaderClasses [i].newInstance();
921       } catch (InstantiationException ex) {
922         // Can't happen: getLoaderClass checked this class is instantiable
923         throw new InternalError(ex.getMessage());
924       } catch (IllegalAccessException ex) {
925         // Can't happen: getLoaderClass checked this class is instantiable
926         throw new InternalError(ex.getMessage());
927       }
928     }
929 
930     Exception lastException = null;
931     Boolean useCaches = shouldUseCaches(urlContent);
932     for (Loader loader : loaders) {
933       boolean loadSynchronously = false;
934       try {
935         // Call setUseCaches(Boolean) by reflection
936         loader.getClass().getMethod("setUseCaches", Boolean.class).invoke(loader, useCaches);
937       } catch (NoSuchMethodException ex) {
938         // If the method setUseCaches doesn't exist, set default cache use if different
939         // from the required one and load models synchronously
940         URLConnection connection = urlContent.getURL().openConnection();
941         loadSynchronously = connection.getDefaultUseCaches() != useCaches;
942       } catch (InvocationTargetException ex) {
943         if (ex instanceof Exception) {
944           lastException = (Exception)ex.getTargetException();
945           continue;
946         } else {
947           ex.printStackTrace();
948         }
949       } catch (Exception ex) {
950         ex.printStackTrace();
951       }
952 
953       try {
954         // Ask loader to ignore lights, fogs...
955         loader.setFlags(loader.getFlags()
956             & ~(Loader.LOAD_LIGHT_NODES | Loader.LOAD_FOG_NODES
957                 | Loader.LOAD_BACKGROUND_NODES | Loader.LOAD_VIEW_GROUPS));
958         // Return the first scene that can be loaded from model URL content
959         Scene scene;
960         if (loadSynchronously) {
961           synchronized (this) {
962             URLConnection connection = urlContent.getURL().openConnection();
963             try {
964               connection.setDefaultUseCaches(useCaches);
965               scene = loader.load(urlContent.getURL());
966             } finally {
967               if (connection.getDefaultUseCaches() == useCaches) {
968                 // Restore the default global value only when it didn't change yet,
969                 // in case an other thread not synchronized on the same lock changed it
970                 connection.setDefaultUseCaches(!useCaches);
971               }
972             }
973           }
974         } else {
975           scene = loader.load(urlContent.getURL());
976         }
977 
978         BranchGroup modelNode = scene.getSceneGroup();
979         // If model doesn't have any child, consider the file as wrong
980         if (modelNode.numChildren() == 0) {
981           throw new IllegalArgumentException("Empty model");
982         }
983 
984         // Update transparency of scene window panes shapes
985         updateShapeNamesAndWindowPanesTransparency(scene);
986         // Turn off lights because some loaders don't take into account the ~LOAD_LIGHT_NODES flag
987         turnOffLightsShareAndModulateTextures(modelNode, new IdentityHashMap<Texture, Texture>());
988         updateDeformableModelHierarchy(modelNode);
989         checkAppearancesName(modelNode);
990         replaceMultipleSharedShapes(modelNode);
991         modelNode.setUserData(content);
992         return modelNode;
993       } catch (IllegalArgumentException ex) {
994         lastException = ex;
995       } catch (IncorrectFormatException ex) {
996         lastException = ex;
997       } catch (ParsingErrorException ex) {
998         lastException = ex;
999       } catch (IOException ex) {
1000         lastException = ex;
1001       } catch (RuntimeException ex) {
1002         // Take into account exceptions of Java 3D 1.5 ImageException class
1003         // in such a way program can run in Java 3D 1.3.1
1004         if (ex.getClass().getName().equals("com.sun.j3d.utils.image.ImageException")) {
1005           lastException = ex;
1006         } else {
1007           throw ex;
1008         }
1009       }
1010     }
1011 
1012     if (lastException instanceof IOException) {
1013       throw (IOException)lastException;
1014     } else if (lastException instanceof IncorrectFormatException) {
1015       IOException incorrectFormatException = new IOException("Incorrect format");
1016       incorrectFormatException.initCause(lastException);
1017       throw incorrectFormatException;
1018     } else if (lastException instanceof ParsingErrorException) {
1019       IOException incorrectFormatException = new IOException("Parsing error");
1020       incorrectFormatException.initCause(lastException);
1021       throw incorrectFormatException;
1022     } else {
1023       IOException otherException = new IOException();
1024       otherException.initCause(lastException);
1025       throw otherException;
1026     }
1027   }
1028 
1029   /**
1030    * Returns <code>true</code> if reading from the given content should be done using caches.
1031    */
shouldUseCaches(URLContent urlContent)1032   private boolean shouldUseCaches(URLContent urlContent) throws IOException {
1033     URLConnection connection = urlContent.getURL().openConnection();
1034     if (OperatingSystem.isWindows()
1035         && (connection instanceof JarURLConnection)) {
1036       JarURLConnection urlConnection = (JarURLConnection)connection;
1037       URL jarFileUrl = urlConnection.getJarFileURL();
1038       if (jarFileUrl.getProtocol().equalsIgnoreCase("file")) {
1039         try {
1040           File file;
1041           try {
1042             file = new File(jarFileUrl.toURI());
1043           } catch (IllegalArgumentException ex) {
1044             // Try a second way to be able to access to files on Windows servers
1045             file = new File(jarFileUrl.getPath());
1046           }
1047           if (file.canWrite()) {
1048             // Refuse to use caches to be able to delete the writable files accessed with jar protocol under Windows,
1049             // as suggested in http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6962459
1050             return false;
1051           }
1052         } catch (URISyntaxException ex) {
1053           IOException ex2 = new IOException();
1054           ex2.initCause(ex);
1055           throw ex2;
1056         }
1057       }
1058     }
1059     return connection.getDefaultUseCaches();
1060   }
1061 
1062   /**
1063    * Updates the name of scene shapes and transparency window panes shapes.
1064    */
1065   @SuppressWarnings("unchecked")
updateShapeNamesAndWindowPanesTransparency(Scene scene)1066   private void updateShapeNamesAndWindowPanesTransparency(Scene scene) {
1067     Map<String, Object> namedObjects = scene.getNamedObjects();
1068     for (Map.Entry<String, Object> entry : namedObjects.entrySet()) {
1069       String name = entry.getKey();
1070       Object value = entry.getValue();
1071       if (value instanceof Node) {
1072         // Assign node name to its user data
1073         ((Node)value).setUserData(name);
1074       }
1075       if (value instanceof Shape3D
1076           && name.startsWith(WINDOW_PANE_SHAPE_PREFIX)) {
1077         Shape3D shape = (Shape3D)value;
1078         Appearance appearance = shape.getAppearance();
1079         if (appearance == null) {
1080           appearance = new Appearance();
1081           shape.setAppearance(appearance);
1082         }
1083         if (appearance.getTransparencyAttributes() == null) {
1084           appearance.setTransparencyAttributes(WINDOW_PANE_TRANSPARENCY_ATTRIBUTES);
1085         }
1086       }
1087     }
1088   }
1089 
1090   /**
1091    * Updates the hierarchy of nodes with intermediate pickable nodes to help deforming models.
1092    */
updateDeformableModelHierarchy(Group group)1093   private void updateDeformableModelHierarchy(Group group) {
1094     // Try to reorganize node hierarchy of mannequin model
1095     if (containsNode(group, MANNEQUIN_ABDOMEN_PREFIX)
1096         && containsNode(group, MANNEQUIN_CHEST_PREFIX)
1097         && containsNode(group, MANNEQUIN_PELVIS_PREFIX)
1098         && containsNode(group, MANNEQUIN_NECK_PREFIX)
1099         && containsNode(group, MANNEQUIN_HEAD_PREFIX)
1100         && containsNode(group, MANNEQUIN_LEFT_SHOULDER_PREFIX)
1101         && containsNode(group, MANNEQUIN_LEFT_ARM_PREFIX)
1102         && containsNode(group, MANNEQUIN_LEFT_ELBOW_PREFIX)
1103         && containsNode(group, MANNEQUIN_LEFT_FOREARM_PREFIX)
1104         && containsNode(group, MANNEQUIN_LEFT_WRIST_PREFIX)
1105         && containsNode(group, MANNEQUIN_LEFT_HAND_PREFIX)
1106         && containsNode(group, MANNEQUIN_LEFT_HIP_PREFIX)
1107         && containsNode(group, MANNEQUIN_LEFT_THIGH_PREFIX)
1108         && containsNode(group, MANNEQUIN_LEFT_KNEE_PREFIX)
1109         && containsNode(group, MANNEQUIN_LEFT_LEG_PREFIX)
1110         && containsNode(group, MANNEQUIN_LEFT_ANKLE_PREFIX)
1111         && containsNode(group, MANNEQUIN_LEFT_FOOT_PREFIX)
1112         && containsNode(group, MANNEQUIN_RIGHT_SHOULDER_PREFIX)
1113         && containsNode(group, MANNEQUIN_RIGHT_ARM_PREFIX)
1114         && containsNode(group, MANNEQUIN_RIGHT_ELBOW_PREFIX)
1115         && containsNode(group, MANNEQUIN_RIGHT_FOREARM_PREFIX)
1116         && containsNode(group, MANNEQUIN_RIGHT_WRIST_PREFIX)
1117         && containsNode(group, MANNEQUIN_RIGHT_HAND_PREFIX)
1118         && containsNode(group, MANNEQUIN_RIGHT_HIP_PREFIX)
1119         && containsNode(group, MANNEQUIN_RIGHT_THIGH_PREFIX)
1120         && containsNode(group, MANNEQUIN_RIGHT_KNEE_PREFIX)
1121         && containsNode(group, MANNEQUIN_RIGHT_LEG_PREFIX)
1122         && containsNode(group, MANNEQUIN_RIGHT_ANKLE_PREFIX)
1123         && containsNode(group, MANNEQUIN_RIGHT_FOOT_PREFIX)) {
1124       // Head
1125       Node head = extractNodes(group, MANNEQUIN_HEAD_PREFIX, null);
1126       TransformGroup headGroup = createPickableTransformGroup(MANNEQUIN_NECK_PREFIX, head);
1127 
1128       // Left arm
1129       Node leftHand = extractNodes(group, MANNEQUIN_LEFT_HAND_PREFIX, null);
1130       TransformGroup leftHandGroup = createPickableTransformGroup(MANNEQUIN_LEFT_WRIST_PREFIX, leftHand);
1131       Node leftForearm = extractNodes(group, MANNEQUIN_LEFT_FOREARM_PREFIX, null);
1132       Node leftWrist = extractNodes(group, MANNEQUIN_LEFT_WRIST_PREFIX, null);
1133       TransformGroup leftForearmGroup = createPickableTransformGroup(MANNEQUIN_LEFT_ELBOW_PREFIX, leftForearm, leftWrist, leftHandGroup);
1134       Node leftArm = extractNodes(group, MANNEQUIN_LEFT_ARM_PREFIX, null);
1135       Node leftElbow = extractNodes(group, MANNEQUIN_LEFT_ELBOW_PREFIX, null);
1136       TransformGroup leftArmGroup = createPickableTransformGroup(MANNEQUIN_LEFT_SHOULDER_PREFIX, leftArm, leftElbow, leftForearmGroup);
1137 
1138       // Right arm
1139       Node rightHand = extractNodes(group, MANNEQUIN_RIGHT_HAND_PREFIX, null);
1140       TransformGroup rightHandGroup = createPickableTransformGroup(MANNEQUIN_RIGHT_WRIST_PREFIX, rightHand);
1141       Node rightForearm = extractNodes(group, MANNEQUIN_RIGHT_FOREARM_PREFIX, null);
1142       Node rightWrist = extractNodes(group, MANNEQUIN_RIGHT_WRIST_PREFIX, null);
1143       TransformGroup rightForearmGroup = createPickableTransformGroup(MANNEQUIN_RIGHT_ELBOW_PREFIX, rightForearm, rightWrist, rightHandGroup);
1144       Node rightArm = extractNodes(group, MANNEQUIN_RIGHT_ARM_PREFIX, null);
1145       Node rightElbow = extractNodes(group, MANNEQUIN_RIGHT_ELBOW_PREFIX, null);
1146       TransformGroup rightArmGroup = createPickableTransformGroup(MANNEQUIN_RIGHT_SHOULDER_PREFIX, rightArm, rightElbow, rightForearmGroup);
1147 
1148       // Chest
1149       Node chest = extractNodes(group, MANNEQUIN_CHEST_PREFIX, null);
1150       Node leftShoulder = extractNodes(group, MANNEQUIN_LEFT_SHOULDER_PREFIX, null);
1151       Node rightShoulder = extractNodes(group, MANNEQUIN_RIGHT_SHOULDER_PREFIX, null);
1152       Node neck = extractNodes(group, MANNEQUIN_NECK_PREFIX, null);
1153       TransformGroup chestGroup = createPickableTransformGroup(MANNEQUIN_ABDOMEN_CHEST_PREFIX, chest, leftShoulder, leftArmGroup, rightShoulder, rightArmGroup, neck, headGroup);
1154 
1155       // Left leg
1156       Node leftFoot = extractNodes(group, MANNEQUIN_LEFT_FOOT_PREFIX, null);
1157       TransformGroup leftFootGroup = createPickableTransformGroup(MANNEQUIN_LEFT_ANKLE_PREFIX, leftFoot);
1158       Node leftLeg = extractNodes(group, MANNEQUIN_LEFT_LEG_PREFIX, null);
1159       Node leftAnkle = extractNodes(group, MANNEQUIN_LEFT_ANKLE_PREFIX, null);
1160       TransformGroup leftLegGroup = createPickableTransformGroup(MANNEQUIN_LEFT_KNEE_PREFIX, leftLeg, leftAnkle, leftFootGroup);
1161       Node leftThigh = extractNodes(group, MANNEQUIN_LEFT_THIGH_PREFIX, null);
1162       Node leftKnee = extractNodes(group, MANNEQUIN_LEFT_KNEE_PREFIX, null);
1163       TransformGroup leftThighGroup = createPickableTransformGroup(MANNEQUIN_LEFT_HIP_PREFIX, leftThigh, leftKnee, leftLegGroup);
1164 
1165       // Right leg
1166       Node rightFoot = extractNodes(group, MANNEQUIN_RIGHT_FOOT_PREFIX, null);
1167       TransformGroup rightFootGroup = createPickableTransformGroup(MANNEQUIN_RIGHT_ANKLE_PREFIX, rightFoot);
1168       Node rightLeg = extractNodes(group, MANNEQUIN_RIGHT_LEG_PREFIX, null);
1169       Node rightAnkle = extractNodes(group, MANNEQUIN_RIGHT_ANKLE_PREFIX, null);
1170       TransformGroup rightLegGroup = createPickableTransformGroup(MANNEQUIN_RIGHT_KNEE_PREFIX, rightLeg, rightAnkle, rightFootGroup);
1171       Node rightThigh = extractNodes(group, MANNEQUIN_RIGHT_THIGH_PREFIX, null);
1172       Node rightKnee = extractNodes(group, MANNEQUIN_RIGHT_KNEE_PREFIX, null);
1173       TransformGroup rightThighGroup = createPickableTransformGroup(MANNEQUIN_RIGHT_HIP_PREFIX, rightThigh, rightKnee, rightLegGroup);
1174 
1175       // Pelvis
1176       Node pelvis = extractNodes(group, MANNEQUIN_PELVIS_PREFIX, null);
1177       Node leftHip = extractNodes(group, MANNEQUIN_LEFT_HIP_PREFIX, null);
1178       Node rightHip = extractNodes(group, MANNEQUIN_RIGHT_HIP_PREFIX, null);
1179       TransformGroup pelvisGroup = createPickableTransformGroup(MANNEQUIN_ABDOMEN_PELVIS_PREFIX, pelvis, leftHip, leftThighGroup, rightHip, rightThighGroup);
1180 
1181       Node abdomen = extractNodes(group, MANNEQUIN_ABDOMEN_PREFIX, null);
1182       group.addChild(abdomen);
1183       group.addChild(chestGroup);
1184       group.addChild(pelvisGroup);
1185     } else {
1186       // Reorganize rotating openings
1187       updateSimpleDeformableModelHierarchy(group, null, HINGE_PREFIX, OPENING_ON_HINGE_PREFIX, WINDOW_PANE_ON_HINGE_PREFIX, MIRROR_ON_HINGE_PREFIX);
1188       updateSimpleDeformableModelHierarchy(group, null, BALL_PREFIX, ARM_ON_BALL_PREFIX, null, null);
1189       // Reorganize sliding openings
1190       updateSimpleDeformableModelHierarchy(group, UNIQUE_RAIL_PREFIX, RAIL_PREFIX, OPENING_ON_RAIL_PREFIX, WINDOW_PANE_ON_RAIL_PREFIX, MIRROR_ON_RAIL_PREFIX);
1191     }
1192   }
1193 
updateSimpleDeformableModelHierarchy(Group group, String uniqueReferenceNodePrefix, String referenceNodePrefix, String openingPrefix, String openingPanePrefix, String openingMirrorPrefix)1194   private void updateSimpleDeformableModelHierarchy(Group group, String uniqueReferenceNodePrefix, String referenceNodePrefix,
1195                                                     String openingPrefix, String openingPanePrefix, String openingMirrorPrefix) {
1196     if (containsNode(group, openingPrefix + 1)
1197         || (openingPanePrefix != null && containsNode(group, openingPanePrefix + 1))
1198         || (openingMirrorPrefix != null && containsNode(group, openingMirrorPrefix + 1))) {
1199       if (containsNode(group, referenceNodePrefix + 1)) {
1200         // Reorganize openings with multiple reference nodes
1201         int i = 1;
1202         do {
1203           Node referenceNode = extractNodes(group, referenceNodePrefix + i, null);
1204           Node opening = extractNodes(group, openingPrefix + i, null);
1205           Node openingPane = openingPanePrefix != null ? extractNodes(group, openingPanePrefix + i, null) : null;
1206           Node openingMirror = openingMirrorPrefix != null ? extractNodes(group, openingMirrorPrefix + i, null) : null;
1207           TransformGroup openingGroup = createPickableTransformGroup(referenceNodePrefix + i, opening, openingPane, openingMirror);
1208           group.addChild(referenceNode);
1209           group.addChild(openingGroup);
1210           i++;
1211         } while (containsNode(group, referenceNodePrefix + i)
1212             && (containsNode(group, openingPrefix + i)
1213                 || (openingPanePrefix != null && containsNode(group, openingPanePrefix + i))
1214                 || (openingMirrorPrefix != null && containsNode(group, openingMirrorPrefix + i))));
1215       } else if (uniqueReferenceNodePrefix != null
1216                  && containsNode(group, uniqueReferenceNodePrefix)) {
1217         // Reorganize openings with a unique reference node
1218         Node referenceNode = extractNodes(group, uniqueReferenceNodePrefix, null);
1219         group.addChild(referenceNode);
1220         int i = 1;
1221         do {
1222           Node opening = extractNodes(group, openingPrefix + i, null);
1223           Node openingPane = extractNodes(group, openingPanePrefix + i, null);
1224           Node openingMirror = extractNodes(group, openingMirrorPrefix + i, null);
1225           group.addChild(createPickableTransformGroup(referenceNodePrefix + i, opening, openingPane, openingMirror));
1226           i++;
1227         } while (containsNode(group, openingPrefix + i)
1228                  || containsNode(group, openingPanePrefix + i)
1229                  || containsNode(group, openingMirrorPrefix + i));
1230       }
1231     }
1232   }
1233 
1234   /**
1235    * Returns <code>true</code> if the given <code>node</code> or a node in its hierarchy
1236    * contains a node which name, stored in user data, starts with <code>prefix</code>.
1237    */
containsNode(Node node, String prefix)1238   public boolean containsNode(Node node, String prefix) {
1239     Object userData = node.getUserData();
1240     if (userData instanceof String
1241         && ((String)userData).startsWith(prefix)) {
1242       return true;
1243     }
1244     if (node instanceof Group) {
1245       Group group = (Group)node;
1246       for (int i = group.numChildren() - 1; i >= 0; i--) {
1247         if (containsNode((Node)group.getChild(i), prefix)) {
1248           return true;
1249         }
1250       }
1251     }
1252     return false;
1253   }
1254 
1255   /**
1256    * Searches among the given <code>node</code> and its children the nodes which name, stored in user data, starts with <code>name</code>,
1257    * then returns a group containing the found nodes.
1258    */
extractNodes(Node node, String name, Group destinationGroup)1259   private Group extractNodes(Node node, String name, Group destinationGroup) {
1260     if (node.getUserData() != null
1261         && ((String)node.getUserData()).startsWith(name)) {
1262       ((Group)node.getParent()).removeChild(node);
1263       if (destinationGroup == null) {
1264         destinationGroup = new Group();
1265       }
1266       destinationGroup.addChild(node);
1267     }
1268     if (node instanceof Group) {
1269       // Enumerate children
1270       Group group = (Group)node;
1271       for (int i = group.numChildren() - 1; i >= 0; i--) {
1272         destinationGroup = extractNodes((Node)group.getChild(i), name, destinationGroup);
1273       }
1274     }
1275     return destinationGroup;
1276   }
1277 
1278   /**
1279    * Returns a pickable group with its <code>children</code> and the given reference node as user data.
1280    */
createPickableTransformGroup(String deformableGroupPrefix, Node ... children)1281   private TransformGroup createPickableTransformGroup(String deformableGroupPrefix, Node ... children) {
1282     TransformGroup transformGroup = new TransformGroup();
1283     transformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
1284     transformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
1285     transformGroup.setCapability(TransformGroup.ENABLE_PICK_REPORTING);
1286     transformGroup.setCapability(TransformGroup.ALLOW_PARENT_READ);
1287     transformGroup.setUserData(deformableGroupPrefix + DEFORMABLE_TRANSFORM_GROUP_SUFFIX);
1288     // Store the node around which objects should turn
1289     for (Node child : children) {
1290       if (child != null) {
1291         transformGroup.addChild(child);
1292       }
1293     }
1294     return transformGroup;
1295   }
1296 
1297   /**
1298    * Return <code>true</code> if the given <code>node</code> or its children contains at least a deformable group.
1299    * @param node  the root of a model
1300    */
containsDeformableNode(Node node)1301   public boolean containsDeformableNode(Node node) {
1302     if (node instanceof TransformGroup
1303         && node.getUserData() instanceof String
1304         && ((String)node.getUserData()).endsWith(DEFORMABLE_TRANSFORM_GROUP_SUFFIX)) {
1305       return true;
1306     } else if (node instanceof Group) {
1307       Enumeration<?> enumeration = ((Group)node).getAllChildren();
1308       while (enumeration.hasMoreElements()) {
1309         if (containsDeformableNode((Node)enumeration.nextElement())) {
1310           return true;
1311         }
1312       }
1313     }
1314     return false;
1315   }
1316 
1317   /**
1318    * Return <code>true</code> if the given <code>node</code> or its children contains is a deformed transformed group.
1319    * @param node  a node
1320    */
isDeformed(Node node)1321   private boolean isDeformed(Node node) {
1322     if (node instanceof TransformGroup
1323         && node.getUserData() instanceof String
1324         && ((String)node.getUserData()).endsWith(DEFORMABLE_TRANSFORM_GROUP_SUFFIX)) {
1325       Transform3D transform = new Transform3D();
1326       ((TransformGroup)node).getTransform(transform);
1327       return (transform.getBestType() & Transform3D.IDENTITY) != Transform3D.IDENTITY;
1328     } else if (node instanceof Group) {
1329       Enumeration<?> enumeration = ((Group)node).getAllChildren();
1330       while (enumeration.hasMoreElements()) {
1331         if (isDeformed((Node)enumeration.nextElement())) {
1332           return true;
1333         }
1334       }
1335     }
1336     return false;
1337   }
1338 
1339   /**
1340    * Turns off light nodes of <code>node</code> children,
1341    * modulates textures if needed and allows shapes to change their pickable property.
1342    */
turnOffLightsShareAndModulateTextures(Node node, Map<Texture, Texture> replacedTextures)1343   private void turnOffLightsShareAndModulateTextures(Node node,
1344                                                       Map<Texture, Texture> replacedTextures) {
1345     if (node instanceof Group) {
1346       // Enumerate children
1347       Enumeration<?> enumeration = ((Group)node).getAllChildren();
1348       while (enumeration.hasMoreElements()) {
1349         turnOffLightsShareAndModulateTextures((Node)enumeration.nextElement(), replacedTextures);
1350       }
1351     } else if (node instanceof Link) {
1352       turnOffLightsShareAndModulateTextures(((Link)node).getSharedGroup(), replacedTextures);
1353     } else if (node instanceof Light) {
1354       ((Light)node).setEnable(false);
1355     } else if (node instanceof Shape3D) {
1356       node.setCapability(Node.ALLOW_PICKABLE_WRITE);
1357       Shape3D shape = ((Shape3D)node);
1358       for (Enumeration<Geometry> it = shape.getAllGeometries(); it.hasMoreElements(); ) {
1359         it.nextElement().setCapability(Geometry.ALLOW_INTERSECT);
1360       }
1361       Appearance appearance = shape.getAppearance();
1362       if (appearance != null) {
1363         Texture texture = appearance.getTexture();
1364         if (texture != null) {
1365           // Share textures data as much as possible requesting TextureManager#shareTexture the less often possible
1366           Texture sharedTexture = replacedTextures.get(texture);
1367           if (sharedTexture == null) {
1368             sharedTexture = TextureManager.getInstance().shareTexture(texture);
1369             replacedTextures.put(texture, sharedTexture);
1370           }
1371           if (sharedTexture != texture) {
1372             appearance.setTexture(sharedTexture);
1373           }
1374           TextureAttributes textureAttributes = appearance.getTextureAttributes();
1375           if (textureAttributes == null) {
1376             // Mix texture and shape color
1377             textureAttributes = new TextureAttributes();
1378             textureAttributes.setTextureMode(TextureAttributes.MODULATE);
1379             appearance.setTextureAttributes(textureAttributes);
1380             // Check shape color is white
1381             Material material = appearance.getMaterial();
1382             if (material == null) {
1383               appearance.setMaterial((Material)DEFAULT_MATERIAL.cloneNodeComponent(true));
1384             } else {
1385               Color3f color = new Color3f();
1386               DEFAULT_MATERIAL.getDiffuseColor(color);
1387               material.setDiffuseColor(color);
1388               DEFAULT_MATERIAL.getAmbientColor(color);
1389               material.setAmbientColor(color);
1390             }
1391           }
1392 
1393           // If texture image supports transparency
1394           if (TextureManager.getInstance().isTextureTransparent(sharedTexture)) {
1395             if (appearance.getTransparencyAttributes() == null) {
1396               // Add transparency attributes to ensure transparency works
1397               appearance.setTransparencyAttributes(
1398                   new TransparencyAttributes(TransparencyAttributes.NICEST, 0));
1399             }
1400           }
1401         }
1402       }
1403     }
1404   }
1405 
1406   /**
1407    * Ensures that all the appearance of the children shapes of the
1408    * given <code>node</code> have a name.
1409    */
checkAppearancesName(Node node)1410   public void checkAppearancesName(Node node) {
1411     // Search appearances used by node shapes keeping their enumeration order
1412     Set<Appearance> appearances = new LinkedHashSet<Appearance>();
1413     searchAppearances(node, appearances);
1414     int i = 0;
1415     for (Appearance appearance : appearances) {
1416       try {
1417         if (appearance.getName() == null) {
1418           appearance.setName("Texture_" + ++i);
1419         }
1420       } catch (NoSuchMethodError ex) {
1421         // Don't support HomeMaterial with Java 3D < 1.4 where appearance name was added
1422         break;
1423       }
1424     }
1425   }
1426 
1427   /**
1428    * Returns the materials used by the children shapes of the given <code>node</code>.
1429    */
getMaterials(Node node)1430   public HomeMaterial [] getMaterials(Node node) {
1431     return getMaterials(node, null);
1432   }
1433 
1434   /**
1435    * Returns the materials used by the children shapes of the given <code>node</code>,
1436    * attributing their <code>creator</code> to them.
1437    */
getMaterials(Node node, String creator)1438   public HomeMaterial [] getMaterials(Node node, String creator) {
1439     // Search appearances used by node shapes
1440     Set<Appearance> appearances = new HashSet<Appearance>();
1441     searchAppearances(node, appearances);
1442     Set<HomeMaterial> materials = new TreeSet<HomeMaterial>(new Comparator<HomeMaterial>() {
1443         public int compare(HomeMaterial m1, HomeMaterial m2) {
1444           String name1 = m1.getName();
1445           String name2 = m2.getName();
1446           if (name1 != null) {
1447             if (name2 != null) {
1448               return name1.compareTo(name2);
1449             } else {
1450               return 1;
1451             }
1452           } else if (name2 != null) {
1453             return -1;
1454           } else {
1455             return 0;
1456           }
1457         }
1458       });
1459     for (Appearance appearance : appearances) {
1460       Integer color = null;
1461       Float   shininess = null;
1462       Material material = appearance.getMaterial();
1463       if (material != null) {
1464         Color3f diffuseColor = new Color3f();
1465         material.getDiffuseColor(diffuseColor);
1466         color = 0xFF000000
1467             | ((int)(diffuseColor.x * 255) << 16)
1468             | ((int)(diffuseColor.y * 255) << 8)
1469             | (int)(diffuseColor.z * 255);
1470         shininess = material.getShininess() / 128;
1471       }
1472       Texture appearanceTexture = appearance.getTexture();
1473       HomeTexture texture = null;
1474       if (appearanceTexture != null) {
1475         URL textureImageUrl = (URL)appearanceTexture.getUserData();
1476         if (textureImageUrl != null) {
1477           Content textureImage = new SimpleURLContent(textureImageUrl);
1478           // Extract image name
1479           String textureImageName = textureImageUrl.getFile();
1480           textureImageName = textureImageName.substring(textureImageName.lastIndexOf('/') + 1);
1481           int lastPoint = textureImageName.lastIndexOf('.');
1482           if (lastPoint != -1) {
1483             textureImageName = textureImageName.substring(0, lastPoint);
1484           }
1485           texture = new HomeTexture(new CatalogTexture(null, textureImageName, textureImage, -1, -1, creator));
1486         }
1487       }
1488       try {
1489         materials.add(new HomeMaterial(appearance.getName(), color, texture, shininess));
1490       } catch (NoSuchMethodError ex) {
1491         // Don't support HomeMaterial with Java 3D < 1.4 where getName was added
1492         return new HomeMaterial [0];
1493       }
1494     }
1495     return materials.toArray(new HomeMaterial [materials.size()]);
1496   }
1497 
searchAppearances(Node node, Set<Appearance> appearances)1498   private void searchAppearances(Node node, Set<Appearance> appearances) {
1499     if (node instanceof Group) {
1500       // Enumerate children
1501       Enumeration<?> enumeration = ((Group)node).getAllChildren();
1502       while (enumeration.hasMoreElements()) {
1503         searchAppearances((Node)enumeration.nextElement(), appearances);
1504       }
1505     } else if (node instanceof Link) {
1506       searchAppearances(((Link)node).getSharedGroup(), appearances);
1507     } else if (node instanceof Shape3D) {
1508       Appearance appearance = ((Shape3D)node).getAppearance();
1509       if (appearance != null) {
1510         appearances.add(appearance);
1511       }
1512     }
1513   }
1514 
1515   /**
1516    * Replaces multiple shared shapes of the given <code>node</code> with one shape with transformed geometries.
1517    */
replaceMultipleSharedShapes(BranchGroup modelRoot)1518   private void replaceMultipleSharedShapes(BranchGroup modelRoot) {
1519     Map<Shape3D, Integer> sharedShapes = new HashMap<Shape3D, Integer>();
1520     searchSharedShapes(modelRoot, sharedShapes, false);
1521     for (Iterator<Map.Entry<Shape3D, Integer>> iterator = sharedShapes.entrySet().iterator(); iterator.hasNext();) {
1522       Map.Entry<Shape3D, Integer> shapeSharingCount = (Map.Entry<Shape3D, Integer>)iterator.next();
1523       if (shapeSharingCount.getValue() > 1) {
1524         List<Transform3D> transformations = new ArrayList<Transform3D>(shapeSharingCount.getValue());
1525         Shape3D shape = shapeSharingCount.getKey();
1526         searchShapeTransformations(modelRoot, shape, transformations, new Transform3D());
1527         // Replace shared shape by a unique shape with transformed geometries
1528         Shape3D newShape = (Shape3D)shape.cloneNode(true);
1529         for (int i = 0; i < newShape.numGeometries(); i++) {
1530           Geometry newGeometry = getTransformedGeometry(newShape.getGeometry(i), transformations);
1531           if (newGeometry == null) {
1532             return;
1533           }
1534           newShape.setGeometry(newGeometry, i);
1535         }
1536         removeSharedShape(modelRoot, shape);
1537         modelRoot.addChild(newShape);
1538       }
1539     }
1540   }
1541 
1542   /**
1543    * Searches all the shapes which are shared among the children of the given <code>node</code>.
1544    */
searchSharedShapes(Node node, Map<Shape3D, Integer> sharedShapes, boolean childOfSharedGroup)1545   private void searchSharedShapes(Node node, Map<Shape3D, Integer> sharedShapes, boolean childOfSharedGroup) {
1546     if (node instanceof Group) {
1547       // Enumerate children
1548       Enumeration<?> enumeration = ((Group)node).getAllChildren();
1549       while (enumeration.hasMoreElements()) {
1550         searchSharedShapes((Node)enumeration.nextElement(), sharedShapes, childOfSharedGroup);
1551       }
1552     } else if (node instanceof Link) {
1553       searchSharedShapes(((Link)node).getSharedGroup(), sharedShapes, true);
1554     } else if (node instanceof Shape3D) {
1555       if (childOfSharedGroup) {
1556         Integer sharingCount = sharedShapes.get(node);
1557         if (sharingCount == null) {
1558           sharedShapes.put((Shape3D)node, 1);
1559         } else {
1560           sharedShapes.put((Shape3D)node, ++sharingCount);
1561         }
1562       }
1563     }
1564   }
1565 
1566   /**
1567    * Searches all the transformations applied to a shared <code>shape</code> child of the given <b>node</b>.
1568    */
searchShapeTransformations(Node node, Shape3D shape, List<Transform3D> transformations, Transform3D parentTransformations)1569   private void searchShapeTransformations(Node node, Shape3D shape, List<Transform3D> transformations, Transform3D parentTransformations) {
1570     if (node instanceof Group) {
1571       if (!(node instanceof TransformGroup)
1572           || !isDeformed(node)) {
1573         if (node instanceof TransformGroup) {
1574           parentTransformations = new Transform3D(parentTransformations);
1575           Transform3D transform = new Transform3D();
1576           ((TransformGroup)node).getTransform(transform);
1577           parentTransformations.mul(transform);
1578         }
1579         // Enumerate children
1580         Enumeration<?> enumeration = ((Group)node).getAllChildren();
1581         while (enumeration.hasMoreElements()) {
1582           searchShapeTransformations((Node)enumeration.nextElement(), shape, transformations, parentTransformations);
1583         }
1584       }
1585     } else if (node instanceof Link) {
1586       searchShapeTransformations(((Link)node).getSharedGroup(), shape, transformations, parentTransformations);
1587     } else if (node == shape) {
1588       transformations.add(parentTransformations);
1589     }
1590   }
1591 
1592   /**
1593    * Returns a new geometry where coordinates are transformed with the given transformations.
1594    */
getTransformedGeometry(Geometry geometry, List<Transform3D> transformations)1595   private Geometry getTransformedGeometry(Geometry geometry,
1596                                           List<Transform3D> transformations) {
1597     if (geometry instanceof GeometryArray) {
1598       GeometryArray geometryArray = (GeometryArray)geometry;
1599       GeometryArray newGeometryArray = null;
1600       boolean normalsDefined = (geometryArray.getVertexFormat() & GeometryArray.NORMALS) != 0;
1601       boolean textureCoordinatesDefined = (geometryArray.getVertexFormat() & GeometryArray.TEXTURE_COORDINATE_2) != 0;
1602 
1603       if (geometryArray instanceof IndexedGeometryArray) {
1604         IndexedGeometryArray indexedGeometryArray = (IndexedGeometryArray)geometryArray;
1605         IndexedGeometryArray newIndexedGeometryArray = null;
1606         if (geometryArray instanceof IndexedLineArray) {
1607           newIndexedGeometryArray = new IndexedLineArray(indexedGeometryArray.getVertexCount() * transformations.size(), indexedGeometryArray.getVertexFormat(), indexedGeometryArray.getIndexCount() * transformations.size());
1608         } else if (geometryArray instanceof IndexedTriangleArray) {
1609           newIndexedGeometryArray = new IndexedTriangleArray(indexedGeometryArray.getVertexCount() * transformations.size(), indexedGeometryArray.getVertexFormat(), indexedGeometryArray.getIndexCount() * transformations.size());
1610         } else if (geometryArray instanceof IndexedQuadArray) {
1611           newIndexedGeometryArray = new IndexedQuadArray(indexedGeometryArray.getVertexCount() * transformations.size(), indexedGeometryArray.getVertexFormat(), indexedGeometryArray.getIndexCount() * transformations.size());
1612         } else if (geometryArray instanceof IndexedGeometryStripArray) {
1613           IndexedGeometryStripArray geometryStripArray = (IndexedGeometryStripArray)geometryArray;
1614           int [] stripIndexCounts = new int [geometryStripArray.getNumStrips()];
1615           geometryStripArray.getStripIndexCounts(stripIndexCounts);
1616           int [] newStripIndexCounts = new int [stripIndexCounts.length * transformations.size()];
1617           int offset = 0;
1618           for (int i = 0; i < transformations.size(); i++) {
1619             for (int j = 0, n = stripIndexCounts.length; j < n; j++) {
1620               newStripIndexCounts [offset + j] = stripIndexCounts [j];
1621             }
1622             offset += stripIndexCounts.length;
1623           }
1624           if (geometryStripArray instanceof IndexedLineStripArray) {
1625             newIndexedGeometryArray = new IndexedLineStripArray(indexedGeometryArray.getVertexCount() * transformations.size(), indexedGeometryArray.getVertexFormat(), indexedGeometryArray.getIndexCount() * transformations.size(), newStripIndexCounts);
1626           } else if (geometryStripArray instanceof IndexedTriangleStripArray) {
1627             newIndexedGeometryArray = new IndexedTriangleStripArray(indexedGeometryArray.getVertexCount() * transformations.size(), indexedGeometryArray.getVertexFormat(), indexedGeometryArray.getIndexCount() * transformations.size(), newStripIndexCounts);
1628           } else if (geometryStripArray instanceof IndexedTriangleFanArray) {
1629             newIndexedGeometryArray = new IndexedTriangleFanArray(indexedGeometryArray.getVertexCount() * transformations.size(), indexedGeometryArray.getVertexFormat(), indexedGeometryArray.getIndexCount(), newStripIndexCounts);
1630           }
1631         }
1632         int offsetIndex = 0;
1633         int offsetVertex = 0;
1634         if (newIndexedGeometryArray != null) {
1635           for (int i = 0; i < transformations.size(); i++) {
1636             for (int j = 0, n = indexedGeometryArray.getIndexCount(); j < n; j++) {
1637               newIndexedGeometryArray.setCoordinateIndex(offsetIndex + j, offsetVertex + indexedGeometryArray.getCoordinateIndex(j));
1638               if (normalsDefined) {
1639                 newIndexedGeometryArray.setNormalIndex(offsetIndex + j, offsetVertex + indexedGeometryArray.getNormalIndex(j));
1640               }
1641               if (textureCoordinatesDefined) {
1642                 newIndexedGeometryArray.setTextureCoordinateIndex(0, offsetIndex + j, offsetVertex + indexedGeometryArray.getTextureCoordinateIndex(0, j));
1643               }
1644             }
1645             offsetIndex += indexedGeometryArray.getIndexCount();
1646             offsetVertex += indexedGeometryArray.getVertexCount();
1647           }
1648           newGeometryArray = newIndexedGeometryArray;
1649         }
1650       } else {
1651         if (geometryArray instanceof LineArray) {
1652           newGeometryArray = new LineArray(geometryArray.getVertexCount() * transformations.size(), geometryArray.getVertexFormat());
1653         } else if (geometryArray instanceof TriangleArray) {
1654           newGeometryArray = new TriangleArray(geometryArray.getVertexCount() * transformations.size(), geometryArray.getVertexFormat());
1655         } else if (geometryArray instanceof QuadArray) {
1656           newGeometryArray = new QuadArray(geometryArray.getVertexCount() * transformations.size(), geometryArray.getVertexFormat());
1657         } else if (geometryArray instanceof GeometryStripArray) {
1658           GeometryStripArray geometryStripArray = (GeometryStripArray)geometryArray;
1659           int [] stripVertexCounts = new int [geometryStripArray.getNumStrips()];
1660           geometryStripArray.getStripVertexCounts(stripVertexCounts);
1661           int [] newStripVertexCounts = new int [stripVertexCounts.length * transformations.size()];
1662           int offset = 0;
1663           for (int i = 0; i < transformations.size(); i++) {
1664             for (int j = 0, n = stripVertexCounts.length; j < n; j++) {
1665               newStripVertexCounts [offset + j] = stripVertexCounts [j];
1666             }
1667             offset += stripVertexCounts.length;
1668           }
1669           if (geometryStripArray instanceof LineStripArray) {
1670             newGeometryArray = new LineStripArray(geometryArray.getVertexCount() * transformations.size(), geometryArray.getVertexFormat(), newStripVertexCounts);
1671           } else if (geometryStripArray instanceof TriangleStripArray) {
1672             newGeometryArray = new TriangleStripArray(geometryArray.getVertexCount() * transformations.size(), geometryArray.getVertexFormat(), newStripVertexCounts);
1673           } else if (geometryStripArray instanceof TriangleFanArray) {
1674             newGeometryArray = new TriangleFanArray(geometryArray.getVertexCount() * transformations.size(), geometryArray.getVertexFormat(), newStripVertexCounts);
1675           }
1676         }
1677       }
1678 
1679       if (newGeometryArray != null) {
1680         if ((geometryArray.getVertexFormat() & GeometryArray.BY_REFERENCE) != 0) {
1681           Point3f  vertex = new Point3f();
1682           Vector3f normal = new Vector3f();
1683           if ((geometryArray.getVertexFormat() & GeometryArray.INTERLEAVED) != 0) {
1684             float [] vertexData = geometryArray.getInterleavedVertices();
1685             int vertexSize = vertexData.length / geometryArray.getVertexCount();
1686             float [] newVertexData = new float [vertexData.length * transformations.size()];
1687             int offset = 0;
1688             for (Transform3D parentTransformations : transformations) {
1689               // Prepare vertices coordinates
1690               for (int index = 0, i = vertexSize - 3, n = geometryArray.getVertexCount(); index < n; index++, i += vertexSize) {
1691                 vertex.x = vertexData [i];
1692                 vertex.y = vertexData [i + 1];
1693                 vertex.z = vertexData [i + 2];
1694                 parentTransformations.transform(vertex);
1695                 newVertexData [offset + i] = vertex.x;
1696                 newVertexData [offset + i + 1] = vertex.y;
1697                 newVertexData [offset + i + 2] = vertex.z;
1698               }
1699               // Prepare texture coordinates
1700               if (textureCoordinatesDefined) {
1701                 for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += vertexSize) {
1702                   newVertexData [offset + i] = vertexData [i];
1703                   newVertexData [offset + i + 1] = vertexData [i + 1];
1704                 }
1705               }
1706               // Prepare normals
1707               if (normalsDefined) {
1708                 for (int index = 0, i = vertexSize - 6, n = geometryArray.getVertexCount(); index < n; index++, i += vertexSize) {
1709                   normal.x = vertexData [i];
1710                   normal.y = vertexData [i + 1];
1711                   normal.z = vertexData [i + 2];
1712                   parentTransformations.transform(normal);
1713                   normal.normalize();
1714                   newVertexData [offset + i] = normal.x;
1715                   newVertexData [offset + i + 1] = normal.y;
1716                   newVertexData [offset + i + 2] = normal.z;
1717                 }
1718               }
1719               offset += vertexData.length;
1720             }
1721             newGeometryArray.setInterleavedVertices(newVertexData);
1722           } else {
1723             // Prepare vertices coordinates
1724             float [] vertexCoordinates = geometryArray.getCoordRefFloat();
1725             float [] newVertexCoordinates = new float [vertexCoordinates.length * transformations.size()];
1726             int offset = 0;
1727             for (Transform3D parentTransformations : transformations) {
1728               for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) {
1729                 vertex.x = vertexCoordinates [i];
1730                 vertex.y = vertexCoordinates [i + 1];
1731                 vertex.z = vertexCoordinates [i + 2];
1732                 parentTransformations.transform(vertex);
1733                 newVertexCoordinates [offset + i] = vertex.x;
1734                 newVertexCoordinates [offset + i + 1] = vertex.y;
1735                 newVertexCoordinates [offset + i + 2] = vertex.z;
1736               }
1737               offset += vertexCoordinates.length;
1738             }
1739             newGeometryArray.setCoordRefFloat(newVertexCoordinates);
1740             if (textureCoordinatesDefined) {
1741               // Prepare texture coordinates
1742               float [] textureCoordinatesArray = geometryArray.getTexCoordRefFloat(0);
1743               float [] newTextureCoordinatesArray = new float [textureCoordinatesArray.length * transformations.size()];
1744               offset = 0;
1745               for (int i = 0; i < transformations.size(); i++) {
1746                 for (int index = 0, j = 0, n = geometryArray.getVertexCount(); index < n; index++, j += 2) {
1747                   newTextureCoordinatesArray [offset + j] = textureCoordinatesArray [j];
1748                   newTextureCoordinatesArray [offset + j + 1] = textureCoordinatesArray [j + 1];
1749                 }
1750                 offset += vertexCoordinates.length;
1751               }
1752               newGeometryArray.setTexCoordRefFloat(0, newTextureCoordinatesArray);
1753             }
1754             if (normalsDefined) {
1755               // Prepare normals
1756               float [] normalCoordinates = geometryArray.getNormalRefFloat();
1757               float [] newNormalCoordinates = new float [normalCoordinates.length * transformations.size()];
1758               offset = 0;
1759               for (Transform3D parentTransformations : transformations) {
1760                 for (int index = 0, i = 0, n = geometryArray.getVertexCount(); index < n; index++, i += 3) {
1761                   normal.x = normalCoordinates [i];
1762                   normal.y = normalCoordinates [i + 1];
1763                   normal.z = normalCoordinates [i + 2];
1764                   parentTransformations.transform(normal);
1765                   normal.normalize();
1766                   newNormalCoordinates [offset + i] = normal.x;
1767                   newNormalCoordinates [offset + i + 1] = normal.y;
1768                   newNormalCoordinates [offset + i + 2] = normal.z;
1769                 }
1770                 offset += vertexCoordinates.length;
1771               }
1772               newGeometryArray.setNormalRefFloat(newNormalCoordinates);
1773             }
1774           }
1775         } else {
1776           // Prepare vertices coordinates
1777           int offset = 0;
1778           for (Transform3D parentTransformations : transformations) {
1779             for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) {
1780               Point3f newVertex = new Point3f();
1781               geometryArray.getCoordinate(index, newVertex);
1782               parentTransformations.transform(newVertex);
1783               newGeometryArray.setCoordinate(offset + index, newVertex);
1784             }
1785             offset += geometryArray.getVertexCount();
1786           }
1787           if (textureCoordinatesDefined) {
1788             // Prepare texture coordinates
1789             offset = 0;
1790             for (int i = 0; i < transformations.size(); i++) {
1791               for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) {
1792                 TexCoord2f newTextureCoordinates = new TexCoord2f();
1793                 geometryArray.getTextureCoordinate(0, index, newTextureCoordinates);
1794                 newGeometryArray.setTextureCoordinate(0, offset + index, newTextureCoordinates);
1795               }
1796               offset += geometryArray.getVertexCount();
1797             }
1798           }
1799           if (normalsDefined) {
1800             // Prepare normals
1801             offset = 0;
1802             for (Transform3D parentTransformations : transformations) {
1803               for (int index = 0, n = geometryArray.getVertexCount(); index < n; index++) {
1804                 Vector3f newNormal = new Vector3f();
1805                 geometryArray.getNormal(index, newNormal);
1806                 parentTransformations.transform(newNormal);
1807                 newNormal.normalize();
1808                 newGeometryArray.setNormal(offset + index, newNormal);
1809               }
1810               offset += geometryArray.getVertexCount();
1811             }
1812           }
1813         }
1814       }
1815       return newGeometryArray;
1816     }
1817     return null;
1818   }
1819 
1820   /**
1821    * Removes the shared shape from the children of the given <code>node</code>.
1822    */
removeSharedShape(Node node, Shape3D shape)1823   private void removeSharedShape(Node node, Shape3D shape) {
1824     if (node instanceof Group) {
1825       if (!(node instanceof TransformGroup)
1826           || !isDeformed(node)) {
1827         Group group = (Group)node;
1828         for (int i = group.numChildren() - 1; i >= 0; i--) {
1829           removeSharedShape(group.getChild(i), shape);
1830         }
1831         if (group.numChildren() == 0
1832             && group.getParent() instanceof Group) {
1833           ((Group)group.getParent()).removeChild(group);
1834         }
1835       }
1836     } else if (node instanceof Link) {
1837       SharedGroup sharedGroup = ((Link)node).getSharedGroup();
1838       removeSharedShape(sharedGroup, shape);
1839       if (sharedGroup.numChildren() == 0) {
1840         ((Group)node.getParent()).removeChild(node);
1841       }
1842     } else if (node == shape) {
1843       ((Group)node.getParent()).removeChild(node);
1844     }
1845   }
1846 
1847   /**
1848    * Returns the AWT shape matching the given cut out shape if not <code>null</code>
1849    * or the 2D area of the 3D shapes children of the <code>node</code>
1850    * projected on its front side. The returned area is normalized in a 1 unit square
1851    * centered at the origin.
1852    */
getFrontArea(String cutOutShape, Node node)1853   Area getFrontArea(String cutOutShape, Node node) {
1854     Area frontArea;
1855     if (cutOutShape != null) {
1856       frontArea = new Area(getShape(cutOutShape));
1857       frontArea.transform(AffineTransform.getScaleInstance(1, -1));
1858       frontArea.transform(AffineTransform.getTranslateInstance(-.5, .5));
1859     } else {
1860       int vertexCount = getVertexCount(node);
1861       if (vertexCount < 1000000) {
1862         Area frontAreaWithHoles = new Area();
1863         computeBottomOrFrontArea(node, frontAreaWithHoles, new Transform3D(), false, false);
1864         // Remove holes and duplicated points
1865         frontArea = new Area();
1866         List<float []> currentPathPoints = new ArrayList<float[]>();
1867         float [] previousRoomPoint = null;
1868         for (PathIterator it = frontAreaWithHoles.getPathIterator(null, 1); !it.isDone(); it.next()) {
1869           float [] areaPoint = new float[2];
1870           switch (it.currentSegment(areaPoint)) {
1871             case PathIterator.SEG_MOVETO :
1872             case PathIterator.SEG_LINETO :
1873               if (previousRoomPoint == null
1874                   || areaPoint [0] != previousRoomPoint [0]
1875                   || areaPoint [1] != previousRoomPoint [1]) {
1876                 currentPathPoints.add(areaPoint);
1877               }
1878               previousRoomPoint = areaPoint;
1879               break;
1880             case PathIterator.SEG_CLOSE :
1881               if (currentPathPoints.get(0) [0] == previousRoomPoint [0]
1882                   && currentPathPoints.get(0) [1] == previousRoomPoint [1]) {
1883                 currentPathPoints.remove(currentPathPoints.size() - 1);
1884               }
1885               if (currentPathPoints.size() > 2) {
1886                 float [][] pathPoints =
1887                     currentPathPoints.toArray(new float [currentPathPoints.size()][]);
1888                 Room subRoom = new Room(pathPoints);
1889                 if (subRoom.getArea() > 0) {
1890                   if (!subRoom.isClockwise()) {
1891                     // Ignore clockwise points that match holes
1892                     GeneralPath currentPath = new GeneralPath();
1893                     currentPath.moveTo(pathPoints [0][0], pathPoints [0][1]);
1894                     for (int i = 1; i < pathPoints.length; i++) {
1895                       currentPath.lineTo(pathPoints [i][0], pathPoints [i][1]);
1896                     }
1897                     currentPath.closePath();
1898                     frontArea.add(new Area(currentPath));
1899                   }
1900                 }
1901               }
1902               currentPathPoints.clear();
1903               previousRoomPoint = null;
1904               break;
1905           }
1906         }
1907         Rectangle2D bounds = frontAreaWithHoles.getBounds2D();
1908         frontArea.transform(AffineTransform.getTranslateInstance(-bounds.getCenterX(), -bounds.getCenterY()));
1909         frontArea.transform(AffineTransform.getScaleInstance(1 / bounds.getWidth(), 1 / bounds.getHeight()));
1910       } else {
1911         frontArea = new Area(new Rectangle2D.Float(-.5f, -.5f, 1, 1));
1912       }
1913     }
1914     return frontArea;
1915   }
1916 
1917 
1918   /**
1919    * Returns the 2D area of the 3D shapes children of the given <code>node</code>
1920    * projected on the floor (plan y = 0).
1921    */
getAreaOnFloor(Node node)1922   public Area getAreaOnFloor(Node node) {
1923     Area modelAreaOnFloor;
1924     int vertexCount = getVertexCount(node);
1925     if (vertexCount < 10000) {
1926       modelAreaOnFloor = new Area();
1927       computeBottomOrFrontArea(node, modelAreaOnFloor, new Transform3D(), true, true);
1928     } else {
1929       List<float []> vertices = new ArrayList<float[]>(vertexCount);
1930       computeVerticesOnFloor(node, vertices, new Transform3D());
1931       if (vertices.size() > 0) {
1932         float [][] surroundingPolygon = getSurroundingPolygon(vertices.toArray(new float [vertices.size()][]));
1933         GeneralPath generalPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, surroundingPolygon.length);
1934         generalPath.moveTo(surroundingPolygon [0][0], surroundingPolygon [0][1]);
1935         for (int i = 0; i < surroundingPolygon.length; i++) {
1936           generalPath.lineTo(surroundingPolygon [i][0], surroundingPolygon [i][1]);
1937         }
1938         generalPath.closePath();
1939         modelAreaOnFloor = new Area(generalPath);
1940       } else {
1941         modelAreaOnFloor = new Area();
1942       }
1943     }
1944     return modelAreaOnFloor;
1945   }
1946 
1947   /**
1948    * Returns the total count of vertices in all geometries.
1949    */
getVertexCount(Node node)1950   private int getVertexCount(Node node) {
1951     int count = 0;
1952     if (node instanceof Group) {
1953       // Enumerate all children
1954       Enumeration<?> enumeration = ((Group)node).getAllChildren();
1955       while (enumeration.hasMoreElements()) {
1956         count += getVertexCount((Node)enumeration.nextElement());
1957       }
1958     } else if (node instanceof Link) {
1959       count = getVertexCount(((Link)node).getSharedGroup());
1960     } else if (node instanceof Shape3D) {
1961       Shape3D shape = (Shape3D)node;
1962       Appearance appearance = shape.getAppearance();
1963       RenderingAttributes renderingAttributes = appearance != null
1964           ? appearance.getRenderingAttributes() : null;
1965       if (renderingAttributes == null
1966           || renderingAttributes.getVisible()) {
1967         for (int i = 0, n = shape.numGeometries(); i < n; i++) {
1968           Geometry geometry = shape.getGeometry(i);
1969           if (geometry instanceof GeometryArray) {
1970             count += ((GeometryArray)geometry).getVertexCount();
1971           }
1972         }
1973       }
1974     }
1975     return count;
1976   }
1977 
1978   /**
1979    * Computes the 2D area on floor or on front side of the 3D shapes children of <code>node</code>.
1980    */
computeBottomOrFrontArea(Node node, Area nodeArea, Transform3D parentTransformations, boolean ignoreTransparentShapes, boolean bottom)1981   private void computeBottomOrFrontArea(Node node,
1982                                         Area nodeArea,
1983                                         Transform3D parentTransformations,
1984                                         boolean ignoreTransparentShapes,
1985                                         boolean bottom) {
1986     if (node instanceof Group) {
1987       if (node instanceof TransformGroup) {
1988         parentTransformations = new Transform3D(parentTransformations);
1989         Transform3D transform = new Transform3D();
1990         ((TransformGroup)node).getTransform(transform);
1991         parentTransformations.mul(transform);
1992       }
1993       // Compute all children
1994       Enumeration<?> enumeration = ((Group)node).getAllChildren();
1995       while (enumeration.hasMoreElements()) {
1996         computeBottomOrFrontArea((Node)enumeration.nextElement(), nodeArea, parentTransformations, ignoreTransparentShapes, bottom);
1997       }
1998     } else if (node instanceof Link) {
1999       computeBottomOrFrontArea(((Link)node).getSharedGroup(), nodeArea, parentTransformations, ignoreTransparentShapes, bottom);
2000     } else if (node instanceof Shape3D) {
2001       Shape3D shape = (Shape3D)node;
2002       Appearance appearance = shape.getAppearance();
2003       RenderingAttributes renderingAttributes = appearance != null
2004           ? appearance.getRenderingAttributes() : null;
2005       TransparencyAttributes transparencyAttributes = appearance != null
2006           ? appearance.getTransparencyAttributes() : null;
2007       if ((renderingAttributes == null
2008             || renderingAttributes.getVisible())
2009           && (!ignoreTransparentShapes
2010               || transparencyAttributes == null
2011               || transparencyAttributes.getTransparency() < 1)) {
2012         // Compute shape geometries area
2013         for (int i = 0, n = shape.numGeometries(); i < n; i++) {
2014           computeBottomOrFrontGeometryArea(shape.getGeometry(i), nodeArea, parentTransformations, bottom);
2015         }
2016       }
2017     }
2018   }
2019 
2020   /**
2021    * Computes the bottom area of a 3D geometry if <code>bottom</code> is <code>true</code>,
2022    * and the front area if not.
2023    */
computeBottomOrFrontGeometryArea(Geometry geometry, Area nodeArea, Transform3D parentTransformations, boolean bottom)2024   private void computeBottomOrFrontGeometryArea(Geometry geometry,
2025                                                 Area nodeArea,
2026                                                 Transform3D parentTransformations,
2027                                                 boolean bottom) {
2028     if (geometry instanceof GeometryArray) {
2029       GeometryArray geometryArray = (GeometryArray)geometry;
2030       int vertexCount = geometryArray.getVertexCount();
2031       float [] vertices = new float [vertexCount * 2];
2032       Point3f vertex = new Point3f();
2033       if ((geometryArray.getVertexFormat() & GeometryArray.BY_REFERENCE) != 0) {
2034         if ((geometryArray.getVertexFormat() & GeometryArray.INTERLEAVED) != 0) {
2035           float [] vertexData = geometryArray.getInterleavedVertices();
2036           int vertexSize = vertexData.length / vertexCount;
2037           // Store vertices coordinates
2038           for (int index = 0, i = vertexSize - 3; index < vertices.length; i += vertexSize) {
2039             vertex.x = vertexData [i];
2040             vertex.y = vertexData [i + 1];
2041             vertex.z = vertexData [i + 2];
2042             parentTransformations.transform(vertex);
2043             vertices [index++] = vertex.x;
2044             if (bottom) {
2045               vertices [index++] = vertex.z;
2046             } else {
2047               vertices [index++] = vertex.y;
2048             }
2049           }
2050         } else {
2051           // Store vertices coordinates
2052           float [] vertexCoordinates = geometryArray.getCoordRefFloat();
2053           for (int index = 0, i = 0; index < vertices.length; i += 3) {
2054             vertex.x = vertexCoordinates [i];
2055             vertex.y = vertexCoordinates [i + 1];
2056             vertex.z = vertexCoordinates [i + 2];
2057             parentTransformations.transform(vertex);
2058             vertices [index++] = vertex.x;
2059             if (bottom) {
2060               vertices [index++] = vertex.z;
2061             } else {
2062               vertices [index++] = vertex.y;
2063             }
2064           }
2065         }
2066       } else {
2067         // Store vertices coordinates
2068         for (int index = 0, i = 0; index < vertices.length; i++) {
2069           geometryArray.getCoordinate(i, vertex);
2070           parentTransformations.transform(vertex);
2071           vertices [index++] = vertex.x;
2072           if (bottom) {
2073             vertices [index++] = vertex.z;
2074           } else {
2075             vertices [index++] = vertex.y;
2076           }
2077         }
2078       }
2079 
2080       // Create path from triangles or quadrilaterals of geometry
2081       GeneralPath geometryPath = null;
2082       if (geometryArray instanceof IndexedGeometryArray) {
2083         if (geometryArray instanceof IndexedTriangleArray) {
2084           IndexedTriangleArray triangleArray = (IndexedTriangleArray)geometryArray;
2085           geometryPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, 1000);
2086           for (int i = 0, triangleIndex = 0, n = triangleArray.getIndexCount(); i < n; i += 3) {
2087             addIndexedTriangleToPath(triangleArray, i, i + 1, i + 2, vertices,
2088                 geometryPath, triangleIndex++, nodeArea);
2089           }
2090         } else if (geometryArray instanceof IndexedQuadArray) {
2091           IndexedQuadArray quadArray = (IndexedQuadArray)geometryArray;
2092           geometryPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, 1000);
2093           for (int i = 0, quadrilateralIndex = 0, n = quadArray.getIndexCount(); i < n; i += 4) {
2094             addIndexedQuadrilateralToPath(quadArray, i, i + 1, i + 2, i + 3, vertices,
2095                 geometryPath, quadrilateralIndex++, nodeArea);
2096           }
2097         } else if (geometryArray instanceof IndexedGeometryStripArray) {
2098           IndexedGeometryStripArray geometryStripArray = (IndexedGeometryStripArray)geometryArray;
2099           int [] stripIndexCounts = new int [geometryStripArray.getNumStrips()];
2100           geometryStripArray.getStripIndexCounts(stripIndexCounts);
2101           geometryPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, 1000);
2102           int initialIndex = 0;
2103 
2104           if (geometryStripArray instanceof IndexedTriangleStripArray) {
2105             for (int strip = 0, triangleIndex = 0; strip < stripIndexCounts.length; strip++) {
2106               for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 2, j = 0; i < n; i++, j++) {
2107                 if (j % 2 == 0) {
2108                   addIndexedTriangleToPath(geometryStripArray, i, i + 1, i + 2, vertices,
2109                       geometryPath, triangleIndex++, nodeArea);
2110                 } else { // Vertices of odd triangles are in reverse order
2111                   addIndexedTriangleToPath(geometryStripArray, i, i + 2, i + 1, vertices,
2112                       geometryPath, triangleIndex++, nodeArea);
2113                 }
2114               }
2115               initialIndex += stripIndexCounts [strip];
2116             }
2117           } else if (geometryStripArray instanceof IndexedTriangleFanArray) {
2118             for (int strip = 0, triangleIndex = 0; strip < stripIndexCounts.length; strip++) {
2119               for (int i = initialIndex, n = initialIndex + stripIndexCounts [strip] - 2; i < n; i++) {
2120                 addIndexedTriangleToPath(geometryStripArray, initialIndex, i + 1, i + 2, vertices,
2121                     geometryPath, triangleIndex++, nodeArea);
2122               }
2123               initialIndex += stripIndexCounts [strip];
2124             }
2125           }
2126         }
2127       } else {
2128         if (geometryArray instanceof TriangleArray) {
2129           TriangleArray triangleArray = (TriangleArray)geometryArray;
2130           geometryPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, 1000);
2131           for (int i = 0, triangleIndex = 0; i < vertexCount; i += 3) {
2132             addTriangleToPath(triangleArray, i, i + 1, i + 2, vertices,
2133                 geometryPath, triangleIndex++, nodeArea);
2134           }
2135         } else if (geometryArray instanceof QuadArray) {
2136           QuadArray quadArray = (QuadArray)geometryArray;
2137           geometryPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, 1000);
2138           for (int i = 0, quadrilateralIndex = 0; i < vertexCount; i += 4) {
2139             addQuadrilateralToPath(quadArray, i, i + 1, i + 2, i + 3, vertices,
2140                 geometryPath, quadrilateralIndex++, nodeArea);
2141           }
2142         } else if (geometryArray instanceof GeometryStripArray) {
2143           GeometryStripArray geometryStripArray = (GeometryStripArray)geometryArray;
2144           int [] stripVertexCounts = new int [geometryStripArray.getNumStrips()];
2145           geometryStripArray.getStripVertexCounts(stripVertexCounts);
2146           geometryPath = new GeneralPath(GeneralPath.WIND_NON_ZERO, 1000);
2147           int initialIndex = 0;
2148 
2149           if (geometryStripArray instanceof TriangleStripArray) {
2150             for (int strip = 0, triangleIndex = 0; strip < stripVertexCounts.length; strip++) {
2151               for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 2, j = 0; i < n; i++, j++) {
2152                 if (j % 2 == 0) {
2153                   addTriangleToPath(geometryStripArray, i, i + 1, i + 2, vertices,
2154                       geometryPath, triangleIndex++, nodeArea);
2155                 } else { // Vertices of odd triangles are in reverse order
2156                   addTriangleToPath(geometryStripArray, i, i + 2, i + 1, vertices,
2157                       geometryPath, triangleIndex++, nodeArea);
2158                 }
2159               }
2160               initialIndex += stripVertexCounts [strip];
2161             }
2162           } else if (geometryStripArray instanceof TriangleFanArray) {
2163             for (int strip = 0, triangleIndex = 0; strip < stripVertexCounts.length; strip++) {
2164               for (int i = initialIndex, n = initialIndex + stripVertexCounts [strip] - 2; i < n; i++) {
2165                 addTriangleToPath(geometryStripArray, initialIndex, i + 1, i + 2, vertices,
2166                     geometryPath, triangleIndex++, nodeArea);
2167               }
2168               initialIndex += stripVertexCounts [strip];
2169             }
2170           }
2171         }
2172       }
2173 
2174       if (geometryPath != null) {
2175         nodeArea.add(new Area(geometryPath));
2176       }
2177     }
2178   }
2179 
2180   /**
2181    * Adds to <code>geometryPath</code> the triangle joining vertices at
2182    * vertexIndex1, vertexIndex2, vertexIndex3 indices.
2183    */
addIndexedTriangleToPath(IndexedGeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, float [] vertices, GeneralPath geometryPath, int triangleIndex, Area nodeArea)2184   private void addIndexedTriangleToPath(IndexedGeometryArray geometryArray,
2185                                     int vertexIndex1, int vertexIndex2, int vertexIndex3,
2186                                     float [] vertices,
2187                                     GeneralPath geometryPath, int triangleIndex, Area nodeArea) {
2188     addTriangleToPath(geometryArray, geometryArray.getCoordinateIndex(vertexIndex1),
2189         geometryArray.getCoordinateIndex(vertexIndex2),
2190         geometryArray.getCoordinateIndex(vertexIndex3), vertices, geometryPath, triangleIndex, nodeArea);
2191   }
2192 
2193   /**
2194    * Adds to <code>geometryPath</code> the quadrilateral joining vertices at
2195    * vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4 indices.
2196    */
addIndexedQuadrilateralToPath(IndexedGeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4, float [] vertices, GeneralPath geometryPath, int quadrilateralIndex, Area nodeArea)2197   private void addIndexedQuadrilateralToPath(IndexedGeometryArray geometryArray,
2198                                          int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4,
2199                                          float [] vertices,
2200                                          GeneralPath geometryPath, int quadrilateralIndex, Area nodeArea) {
2201     addQuadrilateralToPath(geometryArray, geometryArray.getCoordinateIndex(vertexIndex1),
2202         geometryArray.getCoordinateIndex(vertexIndex2),
2203         geometryArray.getCoordinateIndex(vertexIndex3),
2204         geometryArray.getCoordinateIndex(vertexIndex4), vertices, geometryPath, quadrilateralIndex, nodeArea);
2205   }
2206 
2207   /**
2208    * Adds to <code>geometryPath</code> the triangle joining vertices at
2209    * vertexIndex1, vertexIndex2, vertexIndex3 indices,
2210    * only if the triangle has a positive orientation.
2211    */
addTriangleToPath(GeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, float [] vertices, GeneralPath geometryPath, int triangleIndex, Area nodeArea)2212   private void addTriangleToPath(GeometryArray geometryArray,
2213                              int vertexIndex1, int vertexIndex2, int vertexIndex3,
2214                              float [] vertices,
2215                              GeneralPath geometryPath, int triangleIndex, Area nodeArea) {
2216     float xVertex1 = vertices [2 * vertexIndex1];
2217     float yVertex1 = vertices [2 * vertexIndex1 + 1];
2218     float xVertex2 = vertices [2 * vertexIndex2];
2219     float yVertex2 = vertices [2 * vertexIndex2 + 1];
2220     float xVertex3 = vertices [2 * vertexIndex3];
2221     float yVertex3 = vertices [2 * vertexIndex3 + 1];
2222     if ((xVertex2 - xVertex1) * (yVertex3 - yVertex2) - (yVertex2 - yVertex1) * (xVertex3 - xVertex2) > 0) {
2223       if (triangleIndex > 0 && triangleIndex % 1000 == 0) {
2224         // Add now current path to area otherwise area gets too slow
2225         nodeArea.add(new Area(geometryPath));
2226         geometryPath.reset();
2227       }
2228       geometryPath.moveTo(xVertex1, yVertex1);
2229       geometryPath.lineTo(xVertex2, yVertex2);
2230       geometryPath.lineTo(xVertex3, yVertex3);
2231       geometryPath.closePath();
2232     }
2233   }
2234 
2235   /**
2236    * Adds to <code>geometryPath</code> the quadrilateral joining vertices at
2237    * vertexIndex1, vertexIndex2, vertexIndex3, vertexIndex4 indices,
2238    * only if the quadrilateral has a positive orientation.
2239    */
addQuadrilateralToPath(GeometryArray geometryArray, int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4, float [] vertices, GeneralPath geometryPath, int quadrilateralIndex, Area nodeArea)2240   private void addQuadrilateralToPath(GeometryArray geometryArray,
2241                                       int vertexIndex1, int vertexIndex2, int vertexIndex3, int vertexIndex4,
2242                                       float [] vertices,
2243                                       GeneralPath geometryPath, int quadrilateralIndex, Area nodeArea) {
2244     float xVertex1 = vertices [2 * vertexIndex1];
2245     float yVertex1 = vertices [2 * vertexIndex1 + 1];
2246     float xVertex2 = vertices [2 * vertexIndex2];
2247     float yVertex2 = vertices [2 * vertexIndex2 + 1];
2248     float xVertex3 = vertices [2 * vertexIndex3];
2249     float yVertex3 = vertices [2 * vertexIndex3 + 1];
2250     if ((xVertex2 - xVertex1) * (yVertex3 - yVertex2) - (yVertex2 - yVertex1) * (xVertex3 - xVertex2) > 0) {
2251       if (quadrilateralIndex > 0 && quadrilateralIndex % 1000 == 0) {
2252         // Add now current path to area otherwise area gets too slow
2253         nodeArea.add(new Area(geometryPath));
2254         geometryPath.reset();
2255       }
2256       geometryPath.moveTo(xVertex1, yVertex1);
2257       geometryPath.lineTo(xVertex2, yVertex2);
2258       geometryPath.lineTo(xVertex3, yVertex3);
2259       geometryPath.lineTo(vertices [2 * vertexIndex4], vertices [2 * vertexIndex4 + 1]);
2260       geometryPath.closePath();
2261     }
2262   }
2263 
2264   /**
2265    * Computes the vertices coordinates projected on floor of the 3D shapes children of <code>node</code>.
2266    */
computeVerticesOnFloor(Node node, List<float []> vertices, Transform3D parentTransformations)2267   private void computeVerticesOnFloor(Node node, List<float []> vertices, Transform3D parentTransformations) {
2268     if (node instanceof Group) {
2269       if (node instanceof TransformGroup) {
2270         parentTransformations = new Transform3D(parentTransformations);
2271         Transform3D transform = new Transform3D();
2272         ((TransformGroup)node).getTransform(transform);
2273         parentTransformations.mul(transform);
2274       }
2275       // Compute all children
2276       Enumeration<?> enumeration = ((Group)node).getAllChildren();
2277       while (enumeration.hasMoreElements()) {
2278         computeVerticesOnFloor((Node)enumeration.nextElement(), vertices, parentTransformations);
2279       }
2280     } else if (node instanceof Link) {
2281       computeVerticesOnFloor(((Link)node).getSharedGroup(), vertices, parentTransformations);
2282     } else if (node instanceof Shape3D) {
2283       Shape3D shape = (Shape3D)node;
2284       Appearance appearance = shape.getAppearance();
2285       RenderingAttributes renderingAttributes = appearance != null
2286           ? appearance.getRenderingAttributes() : null;
2287       TransparencyAttributes transparencyAttributes = appearance != null
2288           ? appearance.getTransparencyAttributes() : null;
2289       if ((renderingAttributes == null
2290             || renderingAttributes.getVisible())
2291           && (transparencyAttributes == null
2292               || transparencyAttributes.getTransparency() < 1)) {
2293         // Compute shape geometries area
2294         for (int i = 0, n = shape.numGeometries(); i < n; i++) {
2295           Geometry geometry = shape.getGeometry(i);
2296           if (geometry instanceof GeometryArray) {
2297             GeometryArray geometryArray = (GeometryArray)geometry;
2298             int vertexCount = geometryArray.getVertexCount();
2299             Point3f vertex = new Point3f();
2300             if ((geometryArray.getVertexFormat() & GeometryArray.BY_REFERENCE) != 0) {
2301               if ((geometryArray.getVertexFormat() & GeometryArray.INTERLEAVED) != 0) {
2302                 float [] vertexData = geometryArray.getInterleavedVertices();
2303                 int vertexSize = vertexData.length / vertexCount;
2304                 // Store vertices coordinates
2305                 for (int index = 0, j = vertexSize - 3; index < vertexCount; j += vertexSize, index++) {
2306                   vertex.x = vertexData [j];
2307                   vertex.y = vertexData [j + 1];
2308                   vertex.z = vertexData [j + 2];
2309                   parentTransformations.transform(vertex);
2310                   vertices.add(new float [] {vertex.x, vertex.z});
2311                 }
2312               } else {
2313                 // Store vertices coordinates
2314                 float [] vertexCoordinates = geometryArray.getCoordRefFloat();
2315                 for (int index = 0, j = 0; index < vertexCount; j += 3, index++) {
2316                   vertex.x = vertexCoordinates [j];
2317                   vertex.y = vertexCoordinates [j + 1];
2318                   vertex.z = vertexCoordinates [j + 2];
2319                   parentTransformations.transform(vertex);
2320                   vertices.add(new float [] {vertex.x, vertex.z});
2321                 }
2322               }
2323             } else {
2324               // Store vertices coordinates
2325               for (int index = 0, j = 0; index < vertexCount; j++, index++) {
2326                 geometryArray.getCoordinate(j, vertex);
2327                 parentTransformations.transform(vertex);
2328                 vertices.add(new float [] {vertex.x, vertex.z});
2329               }
2330             }
2331           }
2332         }
2333       }
2334     }
2335   }
2336 
2337   /**
2338    * Returns the convex polygon that surrounds the given <code>vertices</code>.
2339    * From Andrew's monotone chain 2D convex hull algorithm described at
2340    * http://softsurfer.com/Archive/algorithm%5F0109/algorithm%5F0109.htm
2341    */
getSurroundingPolygon(float [][] vertices)2342   private float [][] getSurroundingPolygon(float [][] vertices) {
2343     Arrays.sort(vertices, new Comparator<float []> () {
2344         public int compare(float [] vertex1, float [] vertex2) {
2345           if (vertex1 [0] == vertex2 [0]) {
2346             return (int)Math.signum(vertex2 [1] - vertex1 [1]);
2347           } else {
2348             return (int)Math.signum(vertex2 [0] - vertex1 [0]);
2349           }
2350         }
2351       });
2352     float [][] polygon = new float [vertices.length][];
2353     // The output array polygon [] will be used as the stack
2354     int bottom = 0, top = -1; // indices for bottom and top of the stack
2355     int i; // array scan index
2356 
2357     // Get the indices of points with min x-coord and min|max y-coord
2358     int minMin = 0, minMax;
2359     float xmin = vertices [0][0];
2360     for (i = 1; i < vertices.length; i++) {
2361       if (vertices [i][0] != xmin) {
2362         break;
2363       }
2364     }
2365     minMax = i - 1;
2366     if (minMax == vertices.length - 1) {
2367       // Degenerate case: all x-coords == xmin
2368       polygon [++top] = vertices [minMin];
2369       if (vertices [minMax][1] != vertices [minMin][1]) {
2370         // A nontrivial segment
2371         polygon [++top] = vertices [minMax];
2372       }
2373       // Add polygon end point
2374       polygon [++top] = vertices [minMin];
2375       float [][] surroundingPolygon = new float [top + 1][];
2376       System.arraycopy(polygon, 0, surroundingPolygon, 0, surroundingPolygon.length);
2377       return surroundingPolygon;
2378     }
2379 
2380     // Get the indices of points with max x-coord and min|max y-coord
2381     int maxMin, maxMax = vertices.length - 1;
2382     float xMax = vertices [vertices.length - 1][0];
2383     for (i = vertices.length - 2; i >= 0; i--) {
2384       if (vertices [i][0] != xMax) {
2385         break;
2386       }
2387     }
2388     maxMin = i + 1;
2389 
2390     // Compute the lower hull on the stack polygon
2391     polygon [++top] = vertices [minMin]; // push minmin point onto stack
2392     i = minMax;
2393     while (++i <= maxMin) {
2394       // The lower line joins points [minmin] with points [maxmin]
2395       if (isLeft(vertices [minMin], vertices [maxMin], vertices [i]) >= 0 && i < maxMin) {
2396         // ignore points [i] above or on the lower line
2397         continue;
2398       }
2399 
2400       while (top > 0) // There are at least 2 points on the stack
2401       {
2402         // Test if points [i] is left of the line at the stack top
2403         if (isLeft(polygon [top - 1], polygon [top], vertices [i]) > 0) {
2404           break; // points [i] is a new hull vertex
2405         } else {
2406           top--; // pop top point off stack
2407         }
2408       }
2409       polygon [++top] = vertices [i]; // push points [i] onto stack
2410     }
2411 
2412     // Next, compute the upper hull on the stack polygon above the bottom hull
2413     // If distinct xmax points
2414     if (maxMax != maxMin) {
2415       // Push maxmax point onto stack
2416       polygon [++top] = vertices [maxMax];
2417     }
2418     // The bottom point of the upper hull stack
2419     bottom = top;
2420     i = maxMin;
2421     while (--i >= minMax) {
2422       // The upper line joins points [maxmax] with points [minmax]
2423       if (isLeft(vertices [maxMax], vertices [minMax], vertices [i]) >= 0 && i > minMax) {
2424         // Ignore points [i] below or on the upper line
2425         continue;
2426       }
2427 
2428       // At least 2 points on the upper stack
2429       while (top > bottom)
2430       {
2431         // Test if points [i] is left of the line at the stack top
2432         if (isLeft(polygon [top - 1], polygon [top], vertices [i]) > 0) {
2433           // points [i] is a new hull vertex
2434           break;
2435         } else {
2436           // Pop top point off stack
2437           top--;
2438         }
2439       }
2440       // Push points [i] onto stack
2441       polygon [++top] = vertices [i];
2442     }
2443     if (minMax != minMin) {
2444       // Push joining endpoint onto stack
2445       polygon [++top] = vertices [minMin];
2446     }
2447 
2448     float [][] surroundingPolygon = new float [top + 1][];
2449     System.arraycopy(polygon, 0, surroundingPolygon, 0, surroundingPolygon.length);
2450     return surroundingPolygon;
2451   }
2452 
isLeft(float [] vertex0, float [] vertex1, float [] vertex2)2453   private float isLeft(float [] vertex0, float [] vertex1, float [] vertex2) {
2454     return (vertex1 [0] - vertex0 [0]) * (vertex2 [1] - vertex0 [1])
2455          - (vertex2 [0] - vertex0 [0]) * (vertex1 [1] - vertex0 [1]);
2456   }
2457 
2458   /**
2459    * Returns the area on the floor of the given staircase.
2460    */
getAreaOnFloor(HomePieceOfFurniture staircase)2461   public Area getAreaOnFloor(HomePieceOfFurniture staircase) {
2462     if (staircase.getStaircaseCutOutShape() == null) {
2463       throw new IllegalArgumentException("No cut out shape associated to piece");
2464     }
2465     Shape shape = getShape(staircase.getStaircaseCutOutShape());
2466     Area staircaseArea = new Area(shape);
2467     if (staircase.isModelMirrored()) {
2468       staircaseArea = getMirroredArea(staircaseArea);
2469     }
2470     AffineTransform staircaseTransform = AffineTransform.getTranslateInstance(
2471         staircase.getX() - staircase.getWidth() / 2,
2472         staircase.getY() - staircase.getDepth() / 2);
2473     staircaseTransform.concatenate(AffineTransform.getRotateInstance(staircase.getAngle(),
2474         staircase.getWidth() / 2, staircase.getDepth() / 2));
2475     staircaseTransform.concatenate(AffineTransform.getScaleInstance(staircase.getWidth(), staircase.getDepth()));
2476     staircaseArea.transform(staircaseTransform);
2477     return staircaseArea;
2478   }
2479 
2480   /**
2481    * Returns the mirror area of the given <code>area</code>.
2482    */
getMirroredArea(Area area)2483   private Area getMirroredArea(Area area) {
2484     // As applying a -1 scale transform reverses the holes / non holes interpretation of the points,
2485     // we have to create a mirrored shape by parsing points
2486     GeneralPath mirrorPath = new GeneralPath();
2487     float [] point = new float[6];
2488     for (PathIterator it = area.getPathIterator(null); !it.isDone(); it.next()) {
2489       switch (it.currentSegment(point)) {
2490         case PathIterator.SEG_MOVETO :
2491           mirrorPath.moveTo(1 - point[0], point[1]);
2492           break;
2493         case PathIterator.SEG_LINETO :
2494           mirrorPath.lineTo(1 - point[0], point[1]);
2495           break;
2496         case PathIterator.SEG_QUADTO :
2497           mirrorPath.quadTo(1 - point[0], point[1], 1 - point[2], point[3]);
2498           break;
2499         case PathIterator.SEG_CUBICTO :
2500           mirrorPath.curveTo(1 - point[0], point[1], 1 - point[2], point[3], 1 - point[4], point[5]);
2501           break;
2502         case PathIterator.SEG_CLOSE :
2503           mirrorPath.closePath();
2504           break;
2505       }
2506     }
2507     return new Area(mirrorPath);
2508   }
2509 
2510   /**
2511    * Returns the AWT shape matching the given <a href="http://www.w3.org/TR/SVG/paths.html">SVG path shape</a>.
2512    */
getShape(String svgPathShape)2513   public Shape getShape(String svgPathShape) {
2514     return ShapeTools.getShape(svgPathShape);
2515   }
2516 
2517   /**
2518    * An observer that receives model loading notifications.
2519    */
2520   public static interface ModelObserver {
modelUpdated(BranchGroup modelRoot)2521     public void modelUpdated(BranchGroup modelRoot);
2522 
modelError(Exception ex)2523     public void modelError(Exception ex);
2524   }
2525 }
2526