1 /*
  2  * HomeComponent3D.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2015 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 
 21 // Requires core.js
 22 //          scene3d.js
 23 //          SweetHome3D.js
 24 //          ModelManager.js
 25 //          HomePieceOfFurniture3D.js
 26 //          Wall3D.js
 27 //          Room3D.js
 28 //          Polyline3D.js
 29 //          DimensionLine3D.js
 30 //          Label3D.js
 31 //          HomeController3D.js
 32 //          HTMLCanvas3D.js
 33 
 34 /**
 35  * Creates a 3D component that displays <code>home</code> walls, rooms and furniture.
 36  * @param {string} canvasId the id of the HTML canvas associated to this component
 37  * @param {Home} home the home to display in this component
 38  * @param {UserPreferences} preferences user preferences
 39  * @param {Object3DBranchFactory} object3dFactory a factory able to create 3D objects from <code>home</code> items
 40  *            or <code>null</code> to use default one.
 41  *            The <code>createObject3D</code> method of this factory is expected to return 
 42  *            an instance of {@link Object3DBranch} in current implementation.
 43  * @param {HomeController3D} controller the controller that manages modifications in <code>home</code> (optional).
 44  * @constructor   
 45  * @author Emmanuel Puybaret
 46  */
 47 function HomeComponent3D(canvasId, home, preferences, object3dFactory, controller) {
 48   this.home = home;
 49   this.preferences = preferences;
 50   this.object3dFactory = object3dFactory !== null 
 51       ? object3dFactory
 52       : new Object3DBranchFactory(preferences);
 53   this.homeObjects = [];
 54   this.homeObjects3D = [];
 55   this.sceneLights = [];
 56   this.camera = null;
 57   this.windowResizeListener = null;
 58   this.preferencesChangeListener = null;
 59   // Listeners bound to home that updates 3D scene objects
 60   this.cameraChangeListener = null;
 61   this.homeCameraListener = null;
 62   this.groundChangeListener = null;
 63   this.backgroundChangeListener = null;
 64   this.lightColorListener = null;
 65   this.elevationChangeListener = null;
 66   this.wallsAlphaListener = null;
 67   this.selectionListener = null;
 68   this.levelListener = null;
 69   this.levelChangeListener = null;
 70   this.wallListener = null;
 71   this.wallChangeListener = null;  
 72   this.furnitureListener = null;
 73   this.furnitureChangeListener = null;
 74   this.roomListener = null;
 75   this.roomChangeListener = null;
 76   this.polylineChangeListener = null;
 77   this.polylineListener = null;
 78   this.dimensionLineChangeListener = null;
 79   this.dimensionLineListener = null;
 80   this.labelChangeListener = null;
 81   this.labelListener = null;
 82   this.approximateHomeBoundsCache = null;
 83   this.homeHeightCache = null;
 84   this.createComponent3D(canvasId, preferences, controller);
 85 }
 86 
 87 HomeComponent3D["__interfaces"] = ["com.eteks.sweethome3d.viewcontroller.View3D", "com.eteks.sweethome3d.viewcontroller.View"];
 88 
 89 HomeComponent3D.LONG_TOUCH_DELAY = 200; // ms
 90 HomeComponent3D.LONG_TOUCH_DELAY_WHEN_DRAGGING = 400; // ms
 91 HomeComponent3D.LONG_TOUCH_DURATION_AFTER_DELAY = 800; // ms
 92 HomeComponent3D.DOUBLE_TOUCH_DELAY = 500; // ms
 93 
 94 /**
 95  * Creates the 3D canvas associated to the given <code>canvasId</code>.
 96  * @private 
 97  */
 98 HomeComponent3D.prototype.createComponent3D = function(canvasId, preferences, controller) {
 99   this.canvas3D = new HTMLCanvas3D(canvasId);
100   var component3D = this;
101   this.preferencesChangeListener = function(ev) {
102       switch (ev.getPropertyName()) {
103         case "DEFAULT_FONT_NAME" :
104         case "UNIT" :
105           component3D.updateObjects(component3D.home.getDimensionLines());
106           break;
107         case "EDITING_IN_3D_VIEW_ENABLED" :
108           component3D.updateObjectsAndFurnitureGroups(component3D.home.getSelectedItems());
109           break;
110         case "NAVIGATION_PANEL_VISIBLE" :
111           component3D.setNavigationPanelVisible(ev.getNewValue());
112           break;
113       }
114     };
115   if (controller) {
116     this.addMouseListeners(controller, preferences, this.canvas3D);
117     if (preferences !== null) {
118       this.navigationPanelId = this.createNavigationPanel(this.home, preferences, controller);
119       this.setNavigationPanelVisible(preferences.isNavigationPanelVisible());
120       preferences.addPropertyChangeListener("NAVIGATION_PANEL_VISIBLE", this.preferencesChangeListener);
121       preferences.addPropertyChangeListener("EDITING_IN_3D_VIEW_ENABLED", this.preferencesChangeListener);
122     }
123     this.createActions(controller);
124     this.installKeyboardActions();
125   }
126 
127   // Update field of view from current camera
128   this.updateView(this.home.getCamera());
129   // Update point of view from current camera
130   this.updateViewPlatformTransform(this.home.getCamera(), false);
131   // Add camera listeners to update later point of view from camera
132   this.addCameraListeners();
133   
134   this.canvas3D.setScene(this.createSceneTree(true, false));
135   
136   if (preferences !== null) {
137     preferences.addPropertyChangeListener("UNIT", this.preferencesChangeListener);
138     preferences.addPropertyChangeListener("DEFAULT_FONT_NAME", this.preferencesChangeListener);
139   }
140 }
141 
142 /**
143  * Returns the HTML element used to view this component at screen.
144  */
145 HomeComponent3D.prototype.getHTMLElement = function() {
146   return this.canvas3D.getHTMLElement();
147 }
148 
149 /**
150  * Disposes the 3D shapes geometries displayed by this component. 
151  * @package
152  * @ignore
153  */
154 HomeComponent3D.prototype.disposeGeometries = function() {
155   if (this.home.structure) {
156     ModelManager.getInstance().unloadModel(this.home.structure, true);
157   }
158   ModelManager.getInstance().disposeGeometries(this.canvas3D.getScene());
159 } 
160 
161 /**
162  * Updates 3D component aspect and navigation panel after a resize. 
163  * @package
164  * @ignore
165  */
166 HomeComponent3D.prototype.revalidate = function() {
167   var canvas = this.canvas3D.getHTMLElement();
168   var canvasBounds = canvas.getBoundingClientRect();
169   if (this.navigationPanelId != null) {
170     navigationPanelDiv = document.getElementById(this.navigationPanelId);
171     if (navigationPanelDiv !== undefined && navigationPanelDiv.style !== undefined) {
172       navigationPanelDiv.style.left = (canvasBounds.left + window.pageXOffset) + "px";
173       navigationPanelDiv.style.top = (canvasBounds.top + window.pageYOffset) + "px";
174     }
175   }
176   this.canvas3D.updateViewportSize();
177 }
178 
179 /**
180  * Returns the id of a component displayed as navigation panel upon this 3D view.
181  * @private
182  */
183 HomeComponent3D.prototype.createNavigationPanel = function(home, preferences, controller) {
184   // Retrieve body elements with a data-simulated-key attribute
185   var simulatedKeys = this.getSimulatedKeyElements(document.getElementsByTagName("body") [0]);
186   var navigationPanelDiv = null;
187   var innerHtml;
188   try {
189     innerHtml = preferences.getLocalizedString("HomeComponent3D", "navigationPanel.innerHTML");
190   } catch (ex) {
191     innerHtml = 
192           '<img src="' + ZIPTools.getScriptFolder("gl-matrix-min.js") + 'navigationPanel.png"'
193         + '     style="width: 56px; height:59px; margin:5px; user-drag: none; user-select: none; -moz-user-select: none; -webkit-user-drag: none; -webkit-user-select: none; -ms-user-select: none;"' 
194         + '     usemap="#navigationPanelMap"/>'
195         + '<map name="navigationPanelMap" id="navigationPanelMap">'
196         + '  <area shape="poly" coords="28,4,33,8,33,19,22,19,22,8,29,4" data-simulated-key="UP" />'
197         + '  <area shape="poly" coords="4,28,8,23,19,23,19,34,8,34,4,29" data-simulated-key="LEFT" />'
198         + '  <area shape="poly" coords="28,54,33,50,33,39,22,39,22,50,29,54" data-simulated-key="DOWN" />'
199         + '  <area shape="poly" coords="51,28,47,23,36,23,36,34,47,34,51,29" data-simulated-key="RIGHT" />'
200         + '  <area shape="poly" coords="28,22,33,26,33,28,22,28,22,26,29,22" data-simulated-key="PAGE_UP" />'
201         + '  <area shape="poly" coords="28,36,33,32,33,30,22,30,22,32,29,36" data-simulated-key="PAGE_DOWN" />'
202         + '</map>';
203   }
204   var component3D = this;
205   if (innerHtml !== null) {
206     navigationPanelDiv = document.createElement("div");
207     navigationPanelDiv.setAttribute("id", "div" + Math.floor(Math.random() * 1E10));
208     navigationPanelDiv.style.position = "absolute";
209     this.windowResizeListener = function(ev) {
210         component3D.revalidate();
211       };
212     window.addEventListener("resize", this.windowResizeListener);
213     // Search the first existing zIndex among parents
214     var parentZIndex = 0;
215     for (var element = this.canvas3D.getHTMLElement();  
216          element && element.style && isNaN(parentZIndex = parseInt(element.style.zIndex));
217          element = element.parentElement) {
218     }
219     navigationPanelDiv.style.zIndex = isNaN(parentZIndex) ? "1" : (parentZIndex + 1).toString();
220     navigationPanelDiv.style.visibility = "hidden";
221     navigationPanelDiv.innerHTML = innerHtml;
222     simulatedKeys.push.apply(simulatedKeys, this.getSimulatedKeyElements(navigationPanelDiv));
223     var bodyElement = document.getElementsByTagName("body") [0];
224     bodyElement.insertBefore(navigationPanelDiv, bodyElement.firstChild);
225     // Redirect mouse clicks out of div elements to this component 
226     navigationPanelDiv.addEventListener("mousedown", 
227         function(ev) {
228           component3D.mouseListener.mousePressed(ev);
229         });
230   }
231   
232   this.simulatedEventListener = {
233       mousePressed: function(ev) {
234         var simulatedElement = ev.target;
235         var repeatKeyAction = function() {
236             var attribute = simulatedElement.getAttribute("data-simulated-key");
237             var keyName = attribute.substring(attribute.indexOf(":") + 1);
238             var keyStroke = ""; 
239             if (ev.ctrlKey || keyName.indexOf("control ") != -1) {
240               keyStroke += "control ";
241             }
242             if (ev.altKey || keyName.indexOf("alt ") != -1) {
243               keyStroke += "alt ";
244             }
245             if (ev.metaKey || keyName.indexOf("meta ") != -1) {
246               keyStroke += "meta ";
247             }
248             if (ev.shiftKey || keyName.indexOf("shift ") != -1) {
249               keyStroke += "shift ";
250             }
251             keyStroke += "pressed " + keyName;
252             component3D.callAction(ev, keyStroke);
253           };
254         var stopInterval = function(ev) {
255             window.clearInterval(intervalId);
256             if (OperatingSystem.isInternetExplorerOrLegacyEdge()
257                 && window.PointerEvent) {
258               simulatedElement.removeEventListener("pointerup", stopInterval);
259               simulatedElement.removeEventListener("pointerleave", stopInterval);
260             } else {
261               simulatedElement.removeEventListener("touchend", stopInterval);
262               simulatedElement.removeEventListener("mouseup", stopInterval);
263               simulatedElement.removeEventListener("mouseleave", stopInterval);
264             }
265             component3D.mouseListener.windowMouseReleased(ev);
266             ev.stopPropagation();
267           };
268         if (OperatingSystem.isInternetExplorerOrLegacyEdge()
269             && window.PointerEvent) {
270           // Multi touch support for IE and Edge
271           simulatedElement.addEventListener("pointerup", stopInterval);
272           simulatedElement.addEventListener("pointerleave", stopInterval);
273         } else {
274           simulatedElement.addEventListener("touchend", stopInterval);
275           simulatedElement.addEventListener("mouseup", stopInterval);
276           simulatedElement.addEventListener("mouseleave", stopInterval);
277         }
278         repeatKeyAction();
279         var intervalId = window.setInterval(repeatKeyAction, 80);
280       },
281       pointerMousePressed : function(ev) {
282         // Required to avoid click simulation
283         ev.stopPropagation();
284       },
285       touchStarted : function(ev) {
286         // Prevent default behavior to avoid local zooming under iOS >= 15
287         ev.preventDefault(); 
288         component3D.simulatedEventListener.mousePressed(ev);
289       }
290     };
291   for (var i = 0; i < simulatedKeys.length; i++) {
292     // Add a listener that simulates the given key and repeats it until mouse is released 
293     if (OperatingSystem.isInternetExplorerOrLegacyEdge()
294         && window.PointerEvent) {
295       // Multi touch support for IE and Edge
296       simulatedKeys [i].addEventListener("pointerdown", this.simulatedEventListener.mousePressed);
297       simulatedKeys [i].addEventListener("mousedown", this.simulatedEventListener.pointerMousePressed);
298     } else {
299       simulatedKeys [i].addEventListener("touchstart", this.simulatedEventListener.touchStarted);
300       simulatedKeys [i].addEventListener("mousedown", this.simulatedEventListener.mousePressed);
301     }
302   }
303 
304   if (navigationPanelDiv !== null) {
305     return navigationPanelDiv.getAttribute("id");
306   } else {
307     return null;
308   }
309 }
310 
311 /**
312  * Returns the child elements with a <code>data-simulated-key</code> attribute set.
313  * @package
314  * @ignore
315  */
316 HomeComponent3D.prototype.getSimulatedKeyElements = function(element) {
317   var simulatedKeyElements = [];
318   if (element.hasChildNodes()) {
319     for (var i = 0; i < element.childNodes.length; i++) {
320       var child = element.childNodes [i];
321       if (child.hasAttribute
322           && child.hasAttribute("data-simulated-key")) {
323         // Take into account only components with a data-simulated-key attribute 
324         // that contains no colon or that starts with canvas id followed by a colon
325         var simulatedKey = child.getAttribute("data-simulated-key");
326         if (simulatedKey.indexOf(":") === -1
327             || simulatedKey.indexOf(this.canvas3D.getHTMLElement().getAttribute("id") + ":") === 0) {
328           simulatedKeyElements.push(child);
329         }
330       }
331       simulatedKeyElements.push.apply(simulatedKeyElements, this.getSimulatedKeyElements(child));
332     }
333   }
334   return simulatedKeyElements;
335 }
336 
337 /**
338  * Sets the image that will be drawn upon the 3D component shown by this component.
339  * @private
340  */
341 HomeComponent3D.prototype.setNavigationPanelVisible = function(visible) {
342   if (this.navigationPanelId != null) {
343     document.getElementById(this.navigationPanelId).style.visibility = visible ? "visible" : "hidden";
344     if (visible) {
345       this.revalidate();
346     }
347   }
348 }
349 
350 /**
351  * Remove all listeners bound to home that updates 3D scene objects.
352  * @private 
353  */
354 HomeComponent3D.prototype.removeHomeListeners = function() {
355   this.home.removePropertyChangeListener("CAMERA", this.homeCameraListener);
356   var homeEnvironment = this.home.getEnvironment();
357   homeEnvironment.removePropertyChangeListener("SKY_COLOR", this.backgroundChangeListener);
358   homeEnvironment.removePropertyChangeListener("SKY_TEXTURE", this.backgroundChangeListener);
359   homeEnvironment.removePropertyChangeListener("GROUND_COLOR", this.backgroundChangeListener);
360   homeEnvironment.removePropertyChangeListener("GROUND_TEXTURE", this.backgroundChangeListener);
361   homeEnvironment.removePropertyChangeListener("GROUND_COLOR", this.groundChangeListener);
362   homeEnvironment.removePropertyChangeListener("GROUND_TEXTURE", this.groundChangeListener);
363   homeEnvironment.removePropertyChangeListener("BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D", this.groundChangeListener);
364   this.home.removePropertyChangeListener("BACKGROUND_IMAGE", this.groundChangeListener);
365   homeEnvironment.removePropertyChangeListener("LIGHT_COLOR", this.lightColorListener);
366   homeEnvironment.removePropertyChangeListener("WALLS_ALPHA", this.wallsAlphaListener);
367   this.home.getCamera().removePropertyChangeListener(this.cameraChangeListener);
368   this.home.removePropertyChangeListener("CAMERA", this.elevationChangeListener);
369   this.home.getCamera().removePropertyChangeListener(this.elevationChangeListener);
370   this.home.removeSelectionListener(this.selectionListener);
371   this.home.removeLevelsListener(this.levelListener);
372   var levels = this.home.getLevels();
373   for (var i = 0; i < levels.length; i++) {
374     levels[i].removePropertyChangeListener(this.levelChangeListener);
375   }
376   this.home.removeWallsListener(this.wallListener);
377   var walls = this.home.getWalls();
378   for (var i = 0; i < walls.length; i++) {
379     walls[i].removePropertyChangeListener(this.wallChangeListener);
380   }
381   this.home.removeFurnitureListener(this.furnitureListener);
382   var furniture = this.home.getFurniture();
383   for (var i = 0; i < furniture.length; i++) {
384     this.removePropertyChangeListener(furniture [i], this.furnitureChangeListener);
385   }
386   this.home.removeRoomsListener(this.roomListener);
387   var rooms = this.home.getRooms();
388   for (var i = 0; i < rooms.length; i++) {
389     rooms[i].removePropertyChangeListener(this.roomChangeListener);
390   }
391   this.home.removePolylinesListener(this.polylineListener);
392   var polylines = this.home.getPolylines();
393   for (var i = 0; i < polylines.length; i++) {
394     polylines[i].removePropertyChangeListener(this.polylineChangeListener);
395   }
396   this.home.removeDimensionLinesListener(this.dimensionLineListener);
397   var dimensionLines = this.home.getDimensionLines();
398   for (var i = 0; i < dimensionLines.length; i++) {
399     dimensionLines [i].removePropertyChangeListener(this.dimensionLineChangeListener);
400   }
401   this.home.removeLabelsListener(this.labelListener);
402   var labels = this.home.getLabels();
403   for (var i = 0; i < labels.length; i++) {
404     labels[i].removePropertyChangeListener(this.labelChangeListener);
405   }
406 }
407 
408 /**
409  * Remove all mouse listeners bound to the canvas3D and window.
410  * @private 
411  */
412 HomeComponent3D.prototype.removeMouseListeners = function(canvas3D) {
413   if (this.mouseListener) {
414     if (OperatingSystem.isInternetExplorerOrLegacyEdge()
415         && window.PointerEvent) {
416       // Multi touch support for IE and Edge
417       canvas3D.getHTMLElement().removeEventListener("pointerdown", this.mouseListener.pointerPressed);
418       canvas3D.getHTMLElement().removeEventListener("mousedown", this.mouseListener.pointerMousePressed);
419       canvas3D.getHTMLElement().removeEventListener("dblclick", this.mouseListener.mouseDoubleClicked);
420       window.removeEventListener("pointermove", this.mouseListener.windowPointerMoved);
421       window.removeEventListener("pointerup", this.mouseListener.windowPointerReleased);
422     } else {
423       canvas3D.getHTMLElement().removeEventListener("touchstart", this.mouseListener.touchStarted);
424       canvas3D.getHTMLElement().removeEventListener("touchmove", this.mouseListener.touchMoved);
425       canvas3D.getHTMLElement().removeEventListener("touchend", this.mouseListener.touchEnded);
426       canvas3D.getHTMLElement().removeEventListener("mousedown", this.mouseListener.mousePressed);
427       canvas3D.getHTMLElement().removeEventListener("dblclick", this.mouseListener.mouseDoubleClicked);
428       window.removeEventListener("mousemove", this.mouseListener.windowMouseMoved);
429       window.removeEventListener("mouseup", this.mouseListener.windowMouseReleased);
430     }
431     canvas3D.getHTMLElement().removeEventListener("contextmenu", this.mouseListener.contextMenuDisplayed);
432     canvas3D.getHTMLElement().removeEventListener("DOMMouseScroll", this.mouseListener.mouseScrolled);
433     canvas3D.getHTMLElement().removeEventListener("mousewheel", this.mouseListener.mouseWheelMoved);
434   }
435 }
436 
437 /**
438  * Frees listeners and canvas data.
439  */
440 HomeComponent3D.prototype.dispose = function() {
441   this.removeHomeListeners();
442   this.removeMouseListeners(this.canvas3D);
443   if (this.navigationPanelId != null) {
444     this.preferences.removePropertyChangeListener("NAVIGATION_PANEL_VISIBLE", this.preferencesChangeListener);
445     window.removeEventListener("resize", this.windowResizeListener);
446     var simulatedKeys = this.getSimulatedKeyElements(document.getElementsByTagName("body") [0]);
447     for (var i = 0; i < simulatedKeys.length; i++) {
448       if (OperatingSystem.isInternetExplorerOrLegacyEdge()
449           && window.PointerEvent) {
450         simulatedKeys [i].removeEventListener("pointerdown", this.simulatedEventListener.mousePressed);
451         simulatedKeys [i].removeEventListener("mousedown", this.simulatedEventListener.pointerMousePressed);
452       } else {
453         simulatedKeys [i].removeEventListener("touchstart", this.simulatedEventListener.touchStarted);
454         simulatedKeys [i].removeEventListener("mousedown", this.simulatedEventListener.mousePressed);
455       }
456     }
457     var navigationPanel = document.getElementById(this.navigationPanelId);
458     navigationPanel.parentElement.removeChild(navigationPanel);
459     this.navigationPanelId = null;
460   }
461   if (this.preferences !== null) {
462     this.preferences.removePropertyChangeListener("EDITING_IN_3D_VIEW_ENABLED", this.preferencesChangeListener);
463     this.preferences.removePropertyChangeListener("UNIT", this.preferencesChangeListener);
464     this.preferences.removePropertyChangeListener("DEFAULT_FONT_NAME", this.preferencesChangeListener);
465   }
466   this.canvas3D.clear();
467 }
468 
469 /**
470  * Adds listeners to home to update point of view from current camera.
471  * @private 
472  */
473 HomeComponent3D.prototype.addCameraListeners = function() {
474   var component3D = this;
475   var home = this.home;
476   this.cameraChangeListener = function(ev) {
477       if (!component3D.cameraChangeListener.updater) {
478         // Update view transform later to let finish camera changes  
479         component3D.cameraChangeListener.updater = function() {
480             if (component3D.canvas3D) {
481               component3D.updateView(home.getCamera());
482               component3D.updateViewPlatformTransform(home.getCamera(), true);
483             }
484             delete component3D.cameraChangeListener.updater;
485           };
486         setTimeout(component3D.cameraChangeListener.updater, 0);
487       }
488     };
489   home.getCamera().addPropertyChangeListener(this.cameraChangeListener);
490   this.homeCameraListener = function(ev) {
491       component3D.updateView(home.getCamera());
492       component3D.updateViewPlatformTransform(home.getCamera(), false);
493       // Add camera change listener to new active camera
494       ev.getOldValue().removePropertyChangeListener(component3D.cameraChangeListener);
495       home.getCamera().addPropertyChangeListener(component3D.cameraChangeListener);
496     };
497   this.home.addPropertyChangeListener("CAMERA", this.homeCameraListener);
498 }
499 
500 /**
501  * Updates <code>view</code> from <code>camera</code> field of view.
502  * @private 
503  */
504 HomeComponent3D.prototype.updateView = function(camera) {
505   var fieldOfView = camera.getFieldOfView();
506   if (fieldOfView === 0) {
507     fieldOfView = Math.PI * 63 / 180;
508   }
509   this.canvas3D.setFieldOfView(fieldOfView);
510   var frontClipDistance = 2.5;
511   var frontBackDistanceRatio = 500000;
512   if (this.canvas3D.getDepthBits() <= 16) {
513     // It's recommended to keep ratio between back and front clip distances under 3000
514     var frontBackDistanceRatio = 3000;
515     var approximateHomeBounds = this.getApproximateHomeBounds();
516     // If camera is out of home bounds, adjust the front clip distance to the distance to home bounds 
517     if (approximateHomeBounds != null 
518         && !approximateHomeBounds.intersect(vec3.fromValues(camera.getX(), camera.getY(), camera.getZ()))) {
519       var distanceToClosestBoxSide = this.getDistanceToBox(camera.getX(), camera.getY(), camera.getZ(), approximateHomeBounds);
520       if (!isNaN(distanceToClosestBoxSide)) {
521         frontClipDistance = Math.max(frontClipDistance, 0.1 * distanceToClosestBoxSide);
522       }
523     }
524   } else {
525     var homeHeight = this.getHomeHeight();
526     if (camera.getZ() > homeHeight) {
527       frontClipDistance = Math.max(frontClipDistance, (camera.getZ() - homeHeight) / 10);
528     }
529   }    
530   var canvasBounds = this.canvas3D.getHTMLElement().getBoundingClientRect();
531   if (camera.getZ() > 0 && canvasBounds.width !== 0 && canvasBounds.height !== 0) {
532     var halfVerticalFieldOfView = Math.atan(Math.tan(fieldOfView / 2) * canvasBounds.height / canvasBounds.width);
533     var fieldOfViewBottomAngle = camera.getPitch() + halfVerticalFieldOfView;
534     // If the horizon is above the frustrum bottom, take into account the distance to the ground 
535     if (fieldOfViewBottomAngle > 0) {
536       var distanceToGroundAtFieldOfViewBottomAngle = (camera.getZ() / Math.sin(fieldOfViewBottomAngle));
537       frontClipDistance = Math.min(frontClipDistance, 0.35 * distanceToGroundAtFieldOfViewBottomAngle);
538       if (frontClipDistance * frontBackDistanceRatio < distanceToGroundAtFieldOfViewBottomAngle) {
539         // Ensure the ground is always visible at the back clip distance
540         frontClipDistance = distanceToGroundAtFieldOfViewBottomAngle / frontBackDistanceRatio;
541       }
542     }
543   }
544   // Update front and back clip distance 
545   this.canvas3D.setFrontClipDistance(frontClipDistance);
546   this.canvas3D.setBackClipDistance(frontClipDistance * frontBackDistanceRatio);
547 }
548 
549 /**
550  * Returns quickly computed bounds of the objects in home.
551  * @private 
552  */
553 HomeComponent3D.prototype.getApproximateHomeBounds = function() {
554   if (this.approximateHomeBoundsCache === null) {
555     var approximateHomeBounds = null;
556     var furniture = this.home.getFurniture();
557     for (var i = 0; i < furniture.length; i++) {
558       var piece = furniture[i];
559       if (piece.isVisible() 
560           && (piece.getLevel() === null 
561               || piece.getLevel().isViewable())) {
562         var halfMaxDimension = Math.max(piece.getWidthInPlan(), piece.getDepthInPlan()) / 2;
563         var elevation = piece.getGroundElevation();
564         var pieceLocation = vec3.fromValues(
565             piece.getX() - halfMaxDimension, piece.getY() - halfMaxDimension, elevation);
566         if (approximateHomeBounds === null) {
567           approximateHomeBounds = new BoundingBox3D(pieceLocation, pieceLocation);
568         } else {
569           approximateHomeBounds.combine(pieceLocation);
570         }
571         approximateHomeBounds.combine(vec3.fromValues(
572             piece.getX() + halfMaxDimension, piece.getY() + halfMaxDimension, elevation + piece.getHeightInPlan()));
573       }
574     }
575     var walls = this.home.getWalls();
576     for (var i = 0; i < walls.length; i++) {
577       var wall = walls[i];
578       if (wall.getLevel() === null 
579           || wall.getLevel().isViewable()) {
580         var startPoint = vec3.fromValues(wall.getXStart(), wall.getYStart(), 
581             wall.getLevel() !== null ? wall.getLevel().getElevation() : 0);
582         if (approximateHomeBounds === null) {
583           approximateHomeBounds = new BoundingBox3D(startPoint, startPoint);
584         } else {
585           approximateHomeBounds.combine(startPoint);
586         }
587         approximateHomeBounds.combine(vec3.fromValues(wall.getXEnd(), wall.getYEnd(), 
588             startPoint.z + (wall.getHeight() !== null ? wall.getHeight() : this.home.getWallHeight())));
589       }
590     }
591     var rooms = this.home.getRooms();
592     for (var i = 0; i < rooms.length; i++) {
593       var room = rooms[i];
594       if (room.getLevel() === null || room.getLevel().isViewable()) {
595         var center = vec3.fromValues(room.getXCenter(), room.getYCenter(), 
596             room.getLevel() !== null ? room.getLevel().getElevation() : 0);
597         if (approximateHomeBounds === null) {
598           approximateHomeBounds = new BoundingBox3D(center, center);
599         } else {
600           approximateHomeBounds.combine(center);
601         }
602       }
603     }
604     var dimensionLines = this.home.getDimensionLines();
605     for (var i = 0; i < dimensionLines.length; i++) {
606       var dimensionLine = dimensionLines [i];
607       if ((dimensionLine.getLevel() == null
608             || dimensionLine.getLevel().isViewable())
609           && dimensionLine.isVisibleIn3D()) {
610         var levelElevation = dimensionLine.getLevel() != null ? dimensionLine.getLevel().getElevation() : 0;
611         var startPoint = vec3.fromValues(dimensionLine.getXStart(), dimensionLine.getYStart(),
612             levelElevation + dimensionLine.getElevationStart());
613         if (approximateHomeBounds == null) {
614           approximateHomeBounds = new BoundingBox3D(startPoint, startPoint);
615         } else {
616           approximateHomeBounds.combine(startPoint);
617         }
618         approximateHomeBounds.combine(vec3.fromValues(dimensionLine.getXEnd(), dimensionLine.getYEnd(),
619             levelElevation + dimensionLine.getElevationEnd()));
620       }
621     }
622     var labels = this.home.getLabels();
623     for (var i = 0; i < labels.length; i++) {
624       var label = labels[i];
625       if ((label.getLevel() === null 
626             || label.getLevel().isViewable()) 
627           && label.getPitch() !== null) {
628         var center = vec3.fromValues(label.getX(), label.getY(), label.getGroundElevation());
629         if (approximateHomeBounds == null) {
630           approximateHomeBounds = new BoundingBox3D(center, center);
631         } else {
632           approximateHomeBounds.combine(center);
633         }
634       }
635     }
636     var polylines = this.home.getPolylines();
637     for (var i = 0; i < polylines.length; i++) {
638       var polyline = polylines [i];
639       if ((polyline.getLevel() == null
640             || polyline.getLevel().isViewable())
641           && polyline.isVisibleIn3D()) {
642         var points = polyline.getPoints()
643         for (var j = 0; j < points.length; j++) {
644           var point3d = vec3.fromValues(points[j][0], points[j][1], polyline.getGroundElevation());
645           if (approximateHomeBounds == null) {
646             approximateHomeBounds = new BoundingBox3D(point3d, point3d);
647           } else {
648             approximateHomeBounds.combine(point3d);
649           }
650         }
651       }
652     }
653     this.approximateHomeBoundsCache = approximateHomeBounds;
654   }
655   return this.approximateHomeBoundsCache;
656 }
657 
658 /**
659  * Returns the distance between the point at the given coordinates (x,y,z) 
660  * and the closest side of <code>box</code>.
661  * @private
662  */
663 HomeComponent3D.prototype.getDistanceToBox = function (x, y, z, box) {
664   var point = vec3.fromValues(x, y, z);
665   var lower = vec3.create();
666   box.getLower(lower);
667   var upper = vec3.create();
668   box.getUpper(upper);
669   var boxVertices = [
670       vec3.fromValues(lower[0], lower[1], lower[2]), 
671       vec3.fromValues(upper[0], lower[1], lower[2]), 
672       vec3.fromValues(lower[0], upper[1], lower[2]), 
673       vec3.fromValues(upper[0], upper[1], lower[2]), 
674       vec3.fromValues(lower[0], lower[1], upper[2]), 
675       vec3.fromValues(upper[0], lower[1], upper[2]), 
676       vec3.fromValues(lower[0], upper[1], upper[2]), 
677       vec3.fromValues(upper[0], upper[1], upper[2])];
678   var distancesToVertex = new Array(boxVertices.length);
679   for (var i = 0; i < distancesToVertex.length; i++) {
680     distancesToVertex[i] = vec3.squaredDistance(point, boxVertices[i]);
681   }
682   var distancesToSide = [
683       this.getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 3, 2, 2), 
684       this.getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 5, 4, 1), 
685       this.getDistanceToSide(point, boxVertices, distancesToVertex, 0, 2, 6, 4, 0), 
686       this.getDistanceToSide(point, boxVertices, distancesToVertex, 4, 5, 7, 6, 2), 
687       this.getDistanceToSide(point, boxVertices, distancesToVertex, 2, 3, 7, 6, 1), 
688       this.getDistanceToSide(point, boxVertices, distancesToVertex, 1, 3, 7, 5, 0)];
689   var distance = distancesToSide[0];
690   for (var i = 1; i < distancesToSide.length; i++) {
691     distance = Math.min(distance, distancesToSide[i]);
692   }
693   return distance;
694 }
695 
696 /**
697  * Returns the distance between the given <code>point</code> and the plane defined by four vertices.
698  * @private
699  */
700 HomeComponent3D.prototype.getDistanceToSide = function (point, boxVertices, distancesSquaredToVertex, 
701                                                         index1, index2, index3, index4, axis) {
702   switch (axis) {
703     case 0:
704       if (point[1] <= boxVertices[index1][1]) {
705         if (point[2] <= boxVertices[index1][2]) {
706           return Math.sqrt(distancesSquaredToVertex[index1]);
707         } else if (point[2] >= boxVertices[index4][2]) {
708           return Math.sqrt(distancesSquaredToVertex[index4]);
709         } else {
710           return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index4]);
711         }
712       } else if (point[1] >= boxVertices[index2][1]) {
713         if (point[2] <= boxVertices[index2][2]) {
714           return Math.sqrt(distancesSquaredToVertex[index2]);
715         } else if (point[2] >= boxVertices[index3][2]) {
716           return Math.sqrt(distancesSquaredToVertex[index3]);
717         } else {
718           return this.getDistanceToLine(point, boxVertices[index2], boxVertices[index3]);
719         }
720       } else if (point[2] <= boxVertices[index1][2]) {
721         return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index2]);
722       } else if (point[2] >= boxVertices[index4][2]) {
723         return this.getDistanceToLine(point, boxVertices[index3], boxVertices[index4]);
724       }
725       break;
726     case 1 : // Normal along y axis
727       if (point[0] <= boxVertices[index1][0]) {
728         if (point[2] <= boxVertices[index1][2]) {
729           return Math.sqrt(distancesSquaredToVertex[index1]);
730         } else if (point[2] >= boxVertices[index4][2]) {
731           return Math.sqrt(distancesSquaredToVertex[index4]);
732         } else {
733           return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index4]);
734         }
735       } else if (point[0] >= boxVertices[index2][0]) {
736         if (point[2] <= boxVertices[index2][2]) {
737           return Math.sqrt(distancesSquaredToVertex[index2]);
738         } else if (point[2] >= boxVertices[index3][2]) {
739           return Math.sqrt(distancesSquaredToVertex[index3]);
740         } else {
741           return this.getDistanceToLine(point, boxVertices[index2], boxVertices[index3]);
742         }
743       } else if (point[2] <= boxVertices[index1][2]) {
744         return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index2]);
745       } else if (point[2] >= boxVertices[index4][2]) {
746         return this.getDistanceToLine(point, boxVertices[index3], boxVertices[index4]);
747       }
748       break;
749     case 2 : // Normal along z axis
750       if (point[0] <= boxVertices[index1][0]) {
751         if (point[1] <= boxVertices[index1][1]) {
752           return Math.sqrt(distancesSquaredToVertex[index1]);
753         } else if (point[1] >= boxVertices[index4][1]) {
754           return Math.sqrt(distancesSquaredToVertex[index4]);
755         } else {
756           return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index4]);
757         }
758       } else if (point[0] >= boxVertices[index2][0]) {
759         if (point[1] <= boxVertices[index2][1]) {
760           return Math.sqrt(distancesSquaredToVertex[index2]);
761         } else if (point[1] >= boxVertices[index3][1]) {
762           return Math.sqrt(distancesSquaredToVertex[index3]);
763         } else {
764           return this.getDistanceToLine(point, boxVertices[index2], boxVertices[index3]);
765         }
766       } else if (point[1] <= boxVertices[index1][1]) {
767         return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index2]);
768       } else if (point[1] >= boxVertices[index4][1]) {
769         return this.getDistanceToLine(point, boxVertices[index3], boxVertices[index4]);
770       }
771       break;
772   }
773 
774   // Return distance to plane 
775   // from https://fr.wikipedia.org/wiki/Distance_d%27un_point_à_un_plan 
776   var vector1 = vec3.fromValues(boxVertices[index2][0] - boxVertices[index1][0], 
777       boxVertices[index2][1] - boxVertices[index1][1], 
778       boxVertices[index2][2] - boxVertices[index1][2]);
779   var vector2 = vec3.fromValues(boxVertices[index3][0] - boxVertices[index1][0], 
780       boxVertices[index3][1] - boxVertices[index1][1], 
781       boxVertices[index3][2] - boxVertices[index1][2]);
782   var normal = vec3.create();
783   vec3.cross(normal, vector1, vector2);
784   return Math.abs(vec3.dot(normal, vec3.fromValues(boxVertices[index1][0] - point[0], boxVertices[index1][1] - point[1], boxVertices[index1][2] - point[2]))) / vec3.length(normal);
785 }
786 
787 /**
788  * Returns the distance between the given <code>point</code> and the line defined by two points.
789  * @private
790  */
791 HomeComponent3D.prototype.getDistanceToLine = function (point, point1, point2) {
792   // From https://fr.wikipedia.org/wiki/Distance_d%27un_point_à_une_droite#Dans_l.27espace
793   var lineDirection = vec3.fromValues(point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]);
794   var vector = vec3.fromValues(point[0] - point1[0], point[1] - point1[1], point[2] - point1[2]);
795   var crossProduct = vec3.create();
796   vec3.cross(crossProduct, lineDirection, vector);
797   return vec3.length(crossProduct) / vec3.length(lineDirection);
798 }
799 
800 /**
801  * Returns quickly computed height of the home.
802  * @private
803  */
804 HomeComponent3D.prototype.getHomeHeight = function() {
805   if (this.homeHeightCache === null) {
806     var homeHeight = 0;
807     var furniture = this.home.getFurniture();
808     for (var i = 0; i < furniture.length; i++) {
809       var piece = furniture[i];
810       if (piece.isVisible()
811           && (piece.getLevel() == null
812               || piece.getLevel().isViewable())) {
813         homeHeight = Math.max(homeHeight, piece.getGroundElevation() + piece.getHeight());
814       }
815     }
816     var walls = this.home.getWalls();
817     for (var i = 0; i < walls.length; i++) {
818       var wall = walls[i];
819       if (wall.getLevel() == null
820           || wall.getLevel().isViewable()) {
821         var wallElevation = wall.getLevel() != null ? wall.getLevel().getElevation() : 0;
822         if (wall.getHeight() != null) {
823           homeHeight = Math.max(homeHeight, wallElevation + wall.getHeight());
824           if (wall.getHeightAtEnd() != null) {
825             homeHeight = Math.max(homeHeight, wallElevation + wall.getHeightAtEnd());
826           }
827         } else {
828           homeHeight = Math.max(homeHeight, wallElevation + this.home.getWallHeight());
829         }
830       }
831     }
832     var rooms = this.home.getRooms();
833     for (var i = 0; i < rooms.length; i++) {
834       var room = rooms[i];
835       if (room.getLevel() != null
836           && room.getLevel().isViewable()) {
837         homeHeight = Math.max(homeHeight, room.getLevel().getElevation());
838       }
839     }
840     var polylines = this.home.getPolylines();
841     for (var i = 0; i < polylines.length; i++) {
842       var polyline = polylines[i];
843       if (polyline.isVisibleIn3D()
844           && (polyline.getLevel() == null
845               || polyline.getLevel().isViewable())) {
846         homeHeight = Math.max(homeHeight, polyline.getGroundElevation());
847       }
848     }
849     var dimensionLines = this.home.getDimensionLines();
850     for (var i = 0; i < dimensionLines.length; i++) {
851       var dimensionLine = dimensionLines [i];
852       if (dimensionLine.isVisibleIn3D()
853           && (dimensionLine.getLevel() == null
854               || dimensionLine.getLevel().isViewable())) {
855         var levelElevation = dimensionLine.getLevel() != null ? dimensionLine.getLevel().getElevation() : 0;
856         homeHeight = Math.max(homeHeight,
857             levelElevation + Math.max(dimensionLine.getElevationStart(), dimensionLine.getElevationEnd()));
858       }
859     }
860     var labels = this.home.getLabels();
861     for (var i = 0; i < labels.length; i++) {
862       var label = labels[i];
863       if (label.getPitch() != null
864           && (label.getLevel() == null
865               || label.getLevel().isViewable())) {
866         homeHeight = Math.max(homeHeight, label.getGroundElevation());
867       }
868     }
869     this.homeHeightCache = homeHeight;
870   }
871   return this.homeHeightCache;
872 }
873 
874 /**
875  * Updates view transform from <code>camera</code> angles and location.
876  * @private 
877  */
878 HomeComponent3D.prototype.updateViewPlatformTransform = function(camera, updateWithAnimation) {
879   if (updateWithAnimation) {
880     this.moveCameraWithAnimation(camera);
881   } else {
882     delete this.cameraInterpolator; // Stop camera animation if any
883     var viewPlatformTransform = mat4.create();
884     this.computeViewPlatformTransform(viewPlatformTransform, camera.getX(), camera.getY(), 
885         camera.getZ(), camera.getYaw(), camera.getPitch());
886     this.canvas3D.setViewPlatformTransform(viewPlatformTransform);
887   }
888 }
889 
890 /**
891  * Moves the camera to a new location using an animation for smooth moves.
892  * @private 
893  */
894 HomeComponent3D.prototype.moveCameraWithAnimation = function(finalCamera) {
895   if (this.cameraInterpolator === undefined) {
896     this.cameraInterpolator = {initialCamera : null, finalCamera : null, alpha : null};
897   }
898   if (this.cameraInterpolator.finalCamera === null
899       || this.cameraInterpolator.finalCamera.getX() !== finalCamera.getX()
900       || this.cameraInterpolator.finalCamera.getY() !== finalCamera.getY()
901       || this.cameraInterpolator.finalCamera.getZ() !== finalCamera.getZ()
902       || this.cameraInterpolator.finalCamera.getYaw() !== finalCamera.getYaw()
903       || this.cameraInterpolator.finalCamera.getPitch() !== finalCamera.getPitch()) {
904     if (this.cameraInterpolator.alpha === null || this.cameraInterpolator.alpha === 1) {
905       this.cameraInterpolator.initialCamera = new Camera(this.camera.getX(), this.camera.getY(), this.camera.getZ(), 
906           this.camera.getYaw(), this.camera.getPitch(), this.camera.getFieldOfView());
907     } else if (this.cameraInterpolator.alpha < 0.3) {
908       var finalTransformation = mat4.create();
909       // Jump directly to final location
910       this.computeViewPlatformTransform(finalTransformation, this.cameraInterpolator.finalCamera.getX(), this.cameraInterpolator.finalCamera.getY(), this.cameraInterpolator.finalCamera.getZ(), 
911           this.cameraInterpolator.finalCamera.getYaw(), this.cameraInterpolator.finalCamera.getPitch());
912       this.canvas3D.setViewPlatformTransform(finalTransformation);
913       this.cameraInterpolator.initialCamera = this.cameraInterpolator.finalCamera;
914     } else {
915       // Compute initial location from current alpha value 
916       this.cameraInterpolator.initialCamera = new Camera(this.cameraInterpolator.initialCamera.getX() + (this.cameraInterpolator.finalCamera.getX() - this.cameraInterpolator.initialCamera.getX()) * this.cameraInterpolator.alpha, 
917           this.cameraInterpolator.initialCamera.getY() + (this.cameraInterpolator.finalCamera.getY() - this.cameraInterpolator.initialCamera.getY()) * this.cameraInterpolator.alpha, 
918           this.cameraInterpolator.initialCamera.getZ() + (this.cameraInterpolator.finalCamera.getZ() - this.cameraInterpolator.initialCamera.getZ()) * this.cameraInterpolator.alpha,
919           this.cameraInterpolator.initialCamera.getYaw() + (this.cameraInterpolator.finalCamera.getYaw() - this.cameraInterpolator.initialCamera.getYaw()) * this.cameraInterpolator.alpha, 
920           this.cameraInterpolator.initialCamera.getPitch() + (this.cameraInterpolator.finalCamera.getPitch() - this.cameraInterpolator.initialCamera.getPitch()) * this.cameraInterpolator.alpha, 
921           finalCamera.getFieldOfView());
922     }
923     this.cameraInterpolator.finalCamera = new Camera(finalCamera.getX(), finalCamera.getY(), finalCamera.getZ(), 
924         finalCamera.getYaw(), finalCamera.getPitch(), finalCamera.getFieldOfView());
925     // Create an animation that will interpolate camera location 
926     // between initial camera and final camera in 75 ms
927     if (this.cameraInterpolator.alpha === null) {
928       this.cameraInterpolator.animationDuration = 75;
929     }
930     // Start animation now
931     this.cameraInterpolator.startTime = Date.now();
932     this.cameraInterpolator.alpha = 0;
933     var component3D = this;
934     requestAnimationFrame(
935         function() {
936           component3D.interpolateUntilAlphaEquals1();
937         });
938   }
939 }
940 
941 /**
942  * Increases alpha according to elapsed time and interpolates transformation.
943  * @private 
944  */
945 HomeComponent3D.prototype.interpolateUntilAlphaEquals1 = function() {
946   if (this.cameraInterpolator) {
947     var now = Date.now();
948     var alpha = Math.min(1, (now - this.cameraInterpolator.startTime) / this.cameraInterpolator.animationDuration);
949     if (this.cameraInterpolator.alpha !== alpha) {
950       var transform = mat4.create();
951       this.computeTransform(alpha, transform);
952       this.canvas3D.setViewPlatformTransform(transform);
953       this.cameraInterpolator.alpha = alpha;
954     }
955     if (this.cameraInterpolator.alpha < 1) {
956       var component3D = this;
957       requestAnimationFrame(
958           function() {
959             component3D.interpolateUntilAlphaEquals1();
960           });
961     }
962   }
963 }
964 
965 /**
966  * Computes the transformation interpolated between initial and final camera position 
967  * according to alpha. 
968  * @private 
969  */
970 HomeComponent3D.prototype.computeTransform = function(alpha, transform) {
971   this.computeViewPlatformTransform(transform, 
972       this.cameraInterpolator.initialCamera.getX() + (this.cameraInterpolator.finalCamera.getX() - this.cameraInterpolator.initialCamera.getX()) * alpha, 
973       this.cameraInterpolator.initialCamera.getY() + (this.cameraInterpolator.finalCamera.getY() - this.cameraInterpolator.initialCamera.getY()) * alpha, 
974       this.cameraInterpolator.initialCamera.getZ() + (this.cameraInterpolator.finalCamera.getZ() - this.cameraInterpolator.initialCamera.getZ()) * alpha, 
975       this.cameraInterpolator.initialCamera.getYaw() + (this.cameraInterpolator.finalCamera.getYaw() - this.cameraInterpolator.initialCamera.getYaw()) * alpha, 
976       this.cameraInterpolator.initialCamera.getPitch() + (this.cameraInterpolator.finalCamera.getPitch() - this.cameraInterpolator.initialCamera.getPitch()) * alpha);
977 }
978 
979 /**
980  * Updates view transform from camera angles and location.
981  * @private 
982  */
983 HomeComponent3D.prototype.computeViewPlatformTransform = function(transform, cameraX, cameraY, cameraZ, cameraYaw, cameraPitch) {
984   var yawRotation = mat4.create();
985   mat4.fromYRotation(yawRotation, -cameraYaw + Math.PI);
986   
987   var pitchRotation = mat4.create();
988   mat4.fromXRotation(pitchRotation, -cameraPitch);
989   mat4.mul(yawRotation, yawRotation, pitchRotation);
990 
991   mat4.identity(transform);
992   mat4.translate(transform, transform, vec3.fromValues(cameraX, cameraZ, cameraY));
993   mat4.mul(transform, transform, yawRotation);
994   
995   this.camera = new Camera(cameraX, cameraY, cameraZ, cameraYaw, cameraPitch, 0);
996 }
997 
998 /**
999  * Adds mouse listeners to the canvas3D that calls back <code>controller</code> methods.  
1000  * @private 
1001  */
1002 HomeComponent3D.prototype.addMouseListeners = function(controller, preferences, canvas3D) {
1003   var component3D = this; 
1004   var mouseListener = {
1005       initialPointerLocation: null,
1006       lastPointerLocation: null,
1007       touchEventType : false,
1008       buttonPressed : -1,
1009       pointerTouches : {},
1010       lastEventType : null,
1011       lastTargetTouches : [],
1012       distanceLastPinch : -1,
1013       firstTouchStartedTimeStamp: 0,
1014       longTouchActivated: false,
1015       doubleLongTouchActivated: false,
1016       longTouchStartTime: 0,
1017       actionStartedInComponent3D : false,
1018       contextMenuEventType: false,
1019       mousePressed : function(ev) {
1020         if (!mouseListener.touchEventType
1021             && !mouseListener.contextMenuEventType
1022             && ev.button === 0) {
1023           mouseListener.updateCoordinates(ev, "mousePressed");
1024           mouseListener.initialPointerLocation = [ev.canvasX, ev.canvasY];
1025           mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1026           mouseListener.actionStartedInComponent3D = true;
1027           controller.pressMouse(ev.canvasX, ev.canvasY, 
1028               ev.clickCount, mouseListener.isShiftDown(ev), mouseListener.isAlignmentActivated(ev), 
1029               mouseListener.isDuplicationActivated(ev), mouseListener.isMagnetismToggled(ev), View.PointerType.MOUSE);
1030         }
1031         ev.stopPropagation();
1032       },
1033       isShiftDown : function(ev) {
1034         return ev.shiftKey && !ev.altKey && !ev.ctrlKey && !ev.metaKey;
1035       }, 
1036       isAlignmentActivated : function(ev) {
1037         return OperatingSystem.isWindows() || OperatingSystem.isMacOSX() 
1038             ? ev.shiftKey 
1039             : ev.shiftKey && !ev.altKey;
1040       }, 
1041       isDuplicationActivated : function(ev) {
1042         return OperatingSystem.isMacOSX() 
1043             ? ev.altKey 
1044             : ev.ctrlKey;
1045       }, 
1046       isMagnetismToggled : function(ev) {
1047         return OperatingSystem.isWindows() 
1048             ? ev.altKey 
1049             : (OperatingSystem.isMacOSX() 
1050                 ? ev.metaKey 
1051                 : ev.shiftKey && ev.altKey);
1052       },
1053       mouseDoubleClicked: function(ev) {
1054         mouseListener.updateCoordinates(ev, "mouseDoubleClicked");
1055         mouseListener.mousePressed(ev);
1056         mouseListener.actionStartedInComponent3D = false;
1057       },
1058       windowMouseMoved: function(ev) {
1059         if (!mouseListener.touchEventType
1060             && !mouseListener.contextMenuEventType) {
1061           mouseListener.updateCoordinates(ev, "mouseMoved");
1062           if (mouseListener.initialPointerLocation != null 
1063               && !(mouseListener.initialPointerLocation[0] === ev.canvasX 
1064                   && mouseListener.initialPointerLocation[1] === ev.canvasY)) {
1065             mouseListener.initialPointerLocation = null;
1066           }
1067           
1068           if (mouseListener.initialPointerLocation == null
1069               && (ev.buttons === 0 && mouseListener.isInCanvas(ev) 
1070                   || mouseListener.actionStartedInComponent3D)) {
1071             if (controller.isEditingState()) {
1072               controller.moveMouse(ev.canvasX, ev.canvasY);
1073             } else if (mouseListener.actionStartedInComponent3D
1074                        && document.activeElement === component3D.canvas3D.getHTMLElement()) {
1075               mouseListener.moveCamera(ev.canvasX, ev.canvasY, mouseListener.lastPointerLocation [0], mouseListener.lastPointerLocation [1], 
1076                   ev.altKey, ev.shiftKey);
1077             }
1078           }
1079           mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1080         }
1081       }, 
1082       windowMouseReleased: function(ev) {
1083         if (!mouseListener.touchEventType) {
1084           if (mouseListener.lastPointerLocation != null) {
1085             if (mouseListener.actionStartedInComponent3D 
1086                 && document.activeElement === component3D.canvas3D.getHTMLElement()
1087                 && ev.button === 0) {
1088               if (mouseListener.contextMenuEventType) {
1089                 controller.releaseMouse(mouseListener.initialPointerLocation[0], mouseListener.initialPointerLocation[1]);
1090               } else {
1091                 mouseListener.updateCoordinates(ev, "mouseReleased");
1092                 controller.releaseMouse(ev.canvasX, ev.canvasY);
1093               }
1094             }
1095             mouseListener.initialPointerLocation = null;
1096             mouseListener.lastPointerLocation = null;
1097             mouseListener.actionStartedInComponent3D = false;
1098           }
1099         } 
1100         mouseListener.contextMenuEventType = false;
1101       },
1102       pointerPressed : function(ev) {
1103         if (ev.pointerType == "mouse") {
1104           mouseListener.mousePressed(ev);
1105         } else {
1106           // Multi touch support for IE and Edge
1107           mouseListener.copyPointerToTargetTouches(ev, false);
1108           mouseListener.touchStarted(ev);
1109         }
1110       },
1111       pointerMousePressed : function(ev) {
1112         // Required to avoid click simulation
1113         ev.stopPropagation();
1114       },
1115       windowPointerMoved : function(ev) {
1116         if (ev.pointerType == "mouse") {
1117           mouseListener.windowMouseMoved(ev);
1118         } else {
1119           // Multi touch support for IE and Edge
1120           mouseListener.copyPointerToTargetTouches(ev, false) 
1121           mouseListener.touchMoved(ev);
1122         }
1123       },
1124       windowPointerReleased : function(ev) {
1125         if (ev.pointerType == "mouse") {
1126           mouseListener.windowMouseReleased(ev);
1127         } else {
1128           ev.preventDefault();
1129           // Multi touch support for IE and legacy Edge
1130           mouseListener.copyPointerToTargetTouches(ev, true);
1131           mouseListener.touchEnded(ev);
1132         }
1133       },
1134       contextMenuDisplayed : function(ev) {
1135         mouseListener.contextMenuEventType = true;
1136       },
1137       touchStarted: function(ev) {
1138         // Do not prevent default behavior to ensure focus events will be fired if focus changed after a touch event
1139         // but track touch event types to avoid them to be managed also for mousedown and dblclick events
1140         mouseListener.touchEventType = ev.pointerType === undefined;
1141         // Prevent default behavior to ensure a second touchstart event will be received 
1142         // for double taps under iOS >= 15
1143         ev.preventDefault(); 
1144         if (document.activeElement !== component3D.canvas3D.getHTMLElement()) {
1145           // Request focus explicitly since default behavior is disabled
1146           component3D.canvas3D.getHTMLElement().focus();
1147         } 
1148         mouseListener.updateCoordinates(ev, "touchStarted");
1149         if (mouseListener.longTouch != null) {
1150           clearTimeout(mouseListener.longTouch);
1151           mouseListener.longTouch = null;
1152           component3D.stopLongTouchAnimation();
1153         }
1154 
1155         if (ev.targetTouches.length === 1) {
1156           var clickCount = 1;
1157           if (mouseListener.initialPointerLocation != null
1158               && mouseListener.distance(ev.canvasX, ev.canvasY,
1159                   mouseListener.initialPointerLocation [0], mouseListener.initialPointerLocation [1]) < 5 
1160               && ev.timeStamp - mouseListener.firstTouchStartedTimeStamp <= HomeComponent3D.DOUBLE_TOUCH_DELAY) { 
1161             clickCount = 2;
1162             mouseListener.firstTouchStartedTimeStamp = 0;
1163             mouseListener.initialPointerLocation = null;
1164           } else {
1165             mouseListener.firstTouchStartedTimeStamp = ev.timeStamp;
1166             mouseListener.initialPointerLocation = [ev.canvasX, ev.canvasY];
1167           }
1168                 
1169           mouseListener.longTouchActivated = false;
1170           mouseListener.doubleLongTouchActivated = false;
1171           mouseListener.distanceLastPinch = null;
1172           mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1173           mouseListener.actionStartedInComponent3D = true;
1174           mouseListener.longTouchWhenDragged = false;
1175           if (preferences.isEditingIn3DViewEnabled()
1176               && clickCount == 1
1177               && component3D.getClosestSelectableItemAt(ev.canvasX, ev.canvasY) !== null) {
1178             mouseListener.longTouch = setTimeout(function() {
1179               component3D.startLongTouchAnimation(ev.canvasX, ev.canvasY, 
1180                   function() {
1181                     // Simulate shift key press
1182                     mouseListener.longTouchActivated = true;
1183                     controller.setAlignmentActivated(true);
1184                   });
1185                 }, HomeComponent3D.LONG_TOUCH_DELAY);
1186             mouseListener.longTouchStartTime = Date.now();
1187           }
1188               
1189           controller.pressMouse(ev.canvasX, ev.canvasY, 
1190               clickCount, mouseListener.isShiftDown(ev), mouseListener.isAlignmentActivated(ev), 
1191               mouseListener.isDuplicationActivated(ev), mouseListener.isMagnetismToggled(ev), View.PointerType.TOUCH);
1192         } else {
1193           if (mouseListener.longTouchActivated
1194               && ev.targetTouches.length === 2) {
1195             // Simulate alt + shift key press
1196             mouseListener.doubleLongTouchActivated = true;
1197             controller.setDuplicationActivated(true);
1198           } else  {
1199             // Additional touch allows to escape current modification 
1200             controller.escape();
1201           }
1202             
1203           if (ev.targetTouches.length === 2) {
1204             mouseListener.actionStartedInComponent3D = true;
1205             mouseListener.initialPointerLocation = null;
1206             mouseListener.distanceLastPinch = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 
1207                 ev.targetTouches[1].clientX, ev.targetTouches[1].clientY);
1208           }
1209         }
1210       },
1211       touchMoved: function(ev) {
1212         if (mouseListener.actionStartedInComponent3D) {
1213           ev.preventDefault();
1214           ev.stopPropagation();
1215           if (mouseListener.updateCoordinates(ev, "touchMoved")) {
1216             mouseListener.initialPointerLocation = null;
1217             
1218             if (ev.targetTouches.length == 1) {
1219               if (mouseListener.longTouch != null) {
1220                 // Cancel long touch animation only when pointer moved during the past 200 ms
1221                 clearTimeout(mouseListener.longTouch);
1222                 mouseListener.longTouch = null;
1223                 component3D.stopLongTouchAnimation();
1224               }
1225               
1226               if (controller.isEditingState()) {
1227                 if (!mouseListener.doubleLongTouchActivated) {
1228                   controller.moveMouse(ev.canvasX, ev.canvasY);
1229                 }
1230               } else {
1231                 if (component3D.home.getCamera() === component3D.home.getObserverCamera()) {
1232                   mouseListener.moveCamera(-ev.canvasX, -ev.canvasY, 
1233                       -mouseListener.lastPointerLocation [0], -mouseListener.lastPointerLocation [1], false, false);
1234                 } else {
1235                   mouseListener.moveCamera(ev.canvasX, ev.canvasY, 
1236                      mouseListener.lastPointerLocation [0], mouseListener.lastPointerLocation [1], false, false);
1237                 }
1238               }
1239               mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1240             } else if (ev.targetTouches.length == 2
1241                        && mouseListener.distanceLastPinch != null) {
1242               if (controller.isEditingState()) {
1243                 controller.moveMouse(ev.targetTouches[1].clientX, ev.targetTouches[1].clientY);
1244               } else {
1245                 var newDistance = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 
1246                     ev.targetTouches[1].clientX, ev.targetTouches[1].clientY);
1247                 var scaleDifference = newDistance / mouseListener.distanceLastPinch;
1248                 mouseListener.zoomCamera((1 - scaleDifference) * 50, false);
1249                 mouseListener.distanceLastPinch = newDistance;
1250               }
1251             }
1252           }
1253         }
1254       },
1255       touchEnded: function(ev) {
1256         if (mouseListener.actionStartedInComponent3D) {
1257           mouseListener.updateCoordinates(ev, "touchEnded");
1258           if (ev.targetTouches.length == 0) {
1259             if (mouseListener.longTouch != null) {
1260               clearTimeout(mouseListener.longTouch);
1261               mouseListener.longTouch = null;
1262               component3D.stopLongTouchAnimation();
1263             }
1264           
1265             controller.releaseMouse(mouseListener.lastPointerLocation [0], mouseListener.lastPointerLocation [1]);
1266             
1267             if (mouseListener.isLongTouch()) {
1268               // Avoid firing contextmenu event
1269               ev.preventDefault();
1270             }
1271             mouseListener.actionStartedInComponent3D = false;
1272           } else if (ev.targetTouches.length == 1) {
1273             mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1274           } else if (ev.targetTouches.length == 2
1275                      && mouseListener.distanceLastPinch != null) {
1276             // If the user keeps 2 finger on screen after releasing other fingers 
1277             mouseListener.distanceLastPinch = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 
1278                 ev.targetTouches[1].clientX, ev.targetTouches[1].clientY)
1279           }
1280         }
1281       },
1282       copyPointerToTargetTouches : function(ev, touchEnded) {
1283         // Copy the IE and Edge pointer location to ev.targetTouches or ev.changedTouches
1284         if (touchEnded) {
1285           ev.changedTouches = [mouseListener.pointerTouches [ev.pointerId]];
1286           delete mouseListener.pointerTouches [ev.pointerId];
1287         } else {
1288           mouseListener.pointerTouches [ev.pointerId] = {clientX : ev.clientX, clientY : ev.clientY};
1289         }
1290         ev.targetTouches = [];
1291         for (var attribute in mouseListener.pointerTouches) {
1292           if (mouseListener.pointerTouches.hasOwnProperty(attribute)) {
1293             ev.targetTouches.push(mouseListener.pointerTouches [attribute]);
1294           }
1295         }
1296       },
1297       updateCoordinates : function(ev, type) {
1298         // Updates canvasX and canvasY properties and return true if they changed
1299         var rect = component3D.canvas3D.getHTMLElement().getBoundingClientRect();
1300         var updated = true; 
1301         if (type.indexOf("touch") === 0) {
1302           var minDistance = mouseListener.lastEventType == "touchStarted"
1303               ? 5 : 1.5;
1304           var touches;
1305           if (ev.targetTouches.length === 1
1306                 && type == "touchMoved" 
1307                 && mouseListener.distance(mouseListener.lastTargetTouches [0].clientX, mouseListener.lastTargetTouches [0].clientY,
1308                     ev.targetTouches[0].clientX, ev.targetTouches[0].clientY) < minDistance
1309               || ev.targetTouches.length === 0
1310                      && type == "touchEnded" 
1311                      && mouseListener.distance(mouseListener.lastTargetTouches [0].clientX, mouseListener.lastTargetTouches [0].clientY,
1312                          ev.changedTouches[0].clientX, ev.changedTouches[0].clientY) < minDistance) {
1313             touches = mouseListener.lastTargetTouches;
1314             updated = false;
1315           } else {
1316             if (ev.targetTouches.length == 0) {
1317               // touchend case
1318               touches = ev.changedTouches;
1319             } else {
1320               touches = ev.targetTouches;
1321             }
1322             mouseListener.lastEventType = type;
1323           }
1324           
1325           if (touches.length == 1) {
1326             ev.canvasX = touches[0].clientX - rect.left;
1327             ev.canvasY = touches[0].clientY - rect.top;
1328             var rect = component3D.canvas3D.getHTMLElement().getBoundingClientRect();
1329             ev.clientX = touches[0].clientX;
1330             ev.clientY = touches[0].clientY;
1331             ev.button = 0;
1332           } 
1333           ev.clickCount = 1;
1334 
1335           if (updated) {
1336             // Make a copy of touches because old iOS reuse the same ev.targetTouches array between events
1337             mouseListener.lastTargetTouches = [];
1338             for (var i = 0; touches[i] !== undefined; i++) {
1339               mouseListener.lastTargetTouches.push({clientX: touches[i].clientX, clientY: touches[i].clientY});
1340             }
1341           }
1342         } else {
1343           ev.canvasX = ev.clientX - rect.left;
1344           ev.canvasY = ev.clientY - rect.top;
1345         }  
1346         
1347         if (ev.clickCount === undefined) {
1348           if (type == "mouseDoubleClicked") {
1349             ev.clickCount = 2;
1350           } else if (type == "mousePressed" || type == "mouseReleased") {
1351             ev.clickCount = 1;
1352           } else {
1353             ev.clickCount = 0;
1354           }
1355         }
1356         if (type == "mouseWheelMoved") {
1357           ev.wheelRotation = (ev.deltaY !== undefined 
1358               ? ev.deltaX + ev.deltaY 
1359               : -ev.wheelDelta) / 4;
1360         }
1361         
1362         return updated;
1363       },
1364       isLongTouch: function(dragging) {
1365         return Date.now() - mouseListener.longTouchStartTime 
1366             > ((dragging 
1367                 ? HomeComponent3D.LONG_TOUCH_DELAY_WHEN_DRAGGING 
1368                 : HomeComponent3D.LONG_TOUCH_DELAY) + HomeComponent3D.LONG_TOUCH_DURATION_AFTER_DELAY);
1369       },
1370       distance: function(x1, y1, x2, y2) {
1371         return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
1372       },
1373       isInCanvas: function(ev) {
1374         return ev.canvasX >= 0 && ev.canvasX < component3D.canvas3D.getHTMLElement().clientWidth
1375             && ev.canvasY >= 0 && ev.canvasY < component3D.canvas3D.getHTMLElement().clientHeight;
1376       },
1377       mouseScrolled : function(ev) {
1378         mouseListener.zoomCamera(ev.detail, ev.shiftKey);
1379       },
1380       mouseWheelMoved : function(ev) {
1381         ev.preventDefault();
1382         if (!controller.isEditingState()) {
1383           mouseListener.zoomCamera((ev.deltaY !== undefined ? ev.deltaY : -ev.wheelDelta) / 4, ev.shiftKey);
1384         }
1385       },        
1386       zoomCamera : function(delta, shiftKey) {
1387         // Mouse wheel changes camera location 
1388         var delta = -2.5 * delta;
1389         // Multiply delta by 10 if shift is down
1390         if (shiftKey) {
1391           delta *= 5;
1392         } 
1393         controller.moveCamera(delta);
1394       },
1395       moveCamera: function(x, y, lastX, lastY, altKey, shiftKey) {
1396         if (mouseListener.actionStartedInComponent3D
1397             && (mouseListener.lastPointerLocation [0] !== x
1398                 || mouseListener.lastPointerLocation [1] !== y)) {
1399           if (altKey) {
1400             // Mouse move along Y axis while alt is down changes camera location
1401             var delta = 1.25 * (lastY - y);
1402             // Multiply delta by 5 if shift is down
1403             if (shiftKey) {
1404               delta *= 5;
1405             } 
1406             controller.moveCamera(delta);
1407           } else {
1408             var ANGLE_FACTOR = 0.005;
1409             // Mouse move along X axis changes camera yaw 
1410             var yawDelta = ANGLE_FACTOR * (x - lastX);
1411             // Multiply yaw delta by 5 if shift is down
1412             if (shiftKey) {
1413               yawDelta *= 5;
1414             } 
1415             controller.rotateCameraYaw(yawDelta);
1416             
1417             // Mouse move along Y axis changes camera pitch 
1418             var pitchDelta = ANGLE_FACTOR * (y - lastY);
1419             controller.rotateCameraPitch(pitchDelta);
1420           }
1421         }
1422       }
1423     };
1424     
1425   if (OperatingSystem.isInternetExplorerOrLegacyEdge()
1426       && window.PointerEvent) {
1427     // Multi touch support for IE and Edge
1428     // IE and Edge test from https://stackoverflow.com/questions/31757852/how-can-i-detect-internet-explorer-ie-and-microsoft-edge-using-javascript
1429     canvas3D.getHTMLElement().addEventListener("pointerdown", mouseListener.pointerPressed);
1430     canvas3D.getHTMLElement().addEventListener("mousedown", mouseListener.pointerMousePressed);
1431     canvas3D.getHTMLElement().addEventListener("dblclick", mouseListener.mouseDoubleClicked);
1432     // Add pointermove and pointerup event listeners to window to capture pointer events out of the canvas 
1433     window.addEventListener("pointermove", mouseListener.windowPointerMoved);
1434     window.addEventListener("pointerup", mouseListener.windowPointerReleased);
1435   } else {
1436     canvas3D.getHTMLElement().addEventListener("touchstart", mouseListener.touchStarted);
1437     canvas3D.getHTMLElement().addEventListener("touchmove", mouseListener.touchMoved);
1438     canvas3D.getHTMLElement().addEventListener("touchend", mouseListener.touchEnded);
1439     canvas3D.getHTMLElement().addEventListener("mousedown", mouseListener.mousePressed);
1440     canvas3D.getHTMLElement().addEventListener("dblclick", mouseListener.mouseDoubleClicked);
1441     // Add mousemove and mouseup event listeners to window to capture mouse events out of the canvas 
1442     window.addEventListener("mousemove", mouseListener.windowMouseMoved);
1443     window.addEventListener("mouseup", mouseListener.windowMouseReleased);
1444   }
1445   canvas3D.getHTMLElement().addEventListener("contextmenu", mouseListener.contextMenuDisplayed);
1446   canvas3D.getHTMLElement().addEventListener("DOMMouseScroll", mouseListener.mouseScrolled);
1447   canvas3D.getHTMLElement().addEventListener("mousewheel", mouseListener.mouseWheelMoved);
1448 
1449   this.mouseListener = mouseListener;
1450 }
1451 
1452 /**
1453  * @private 
1454  */
1455 HomeComponent3D.prototype.startLongTouchAnimation = function(x, y, animationPostTask) {
1456   if (this.touchOverlay === undefined) {
1457     this.touchOverlay = document.createElement("div");
1458     this.touchOverlay.id = "touch-overlay-timer";
1459     this.touchOverlay.classList.add("touch-overlay-timer");
1460     this.touchOverlay.style.position = "absolute";
1461     this.touchOverlay.style.top = "0px";
1462     this.touchOverlay.style.left = "0px";
1463     this.touchOverlay.innerHTML = '<div class="touch-overlay-timer-content"></div><div class="touch-overlay-timer-bg"></div><div class="touch-overlay-timer-hidder"></div><div class="touch-overlay-timer-loader1"></div><div class="touch-overlay-timer-loader2"></div>';
1464     document.body.appendChild(this.touchOverlay);
1465     for (var i = 0; i < this.touchOverlay.children.length; i++) {
1466       var item = this.touchOverlay.children.item(i);
1467       if (item.classList.contains("overlay-timer-loader1")
1468           || item.classList.contains("overlay-timer-loader2")) {
1469         item.style.borderTopColor = "#0000B5";
1470         item.style.borderRightColor = "#0000B5";
1471       }
1472       if (item.classList.contains("touch-overlay-timer-content")) {
1473         item.style.color = "#000000";
1474         item.innerHTML = "<span style='font-weight: bold; font-family: sans-serif; font-size: 140%; line-height: 90%'>⇪</span>";
1475       }
1476       item.style.animationDuration = (PlanComponent.LONG_TOUCH_DURATION_AFTER_DELAY) + "ms";
1477     }
1478   }
1479   this.touchOverlay.style.visibility = "visible";
1480   this.touchOverlay.style.left = (this.getHTMLElement().getBoundingClientRect().left + x - this.touchOverlay.clientWidth / 2) + "px";
1481   this.touchOverlay.style.top = (this.getHTMLElement().getBoundingClientRect().top + y - this.touchOverlay.clientHeight - 40) + "px";
1482   for (var i = 0; i < this.touchOverlay.children.length; i++) {
1483     this.touchOverlay.children.item(i).classList.add("animated");
1484   }
1485   if (animationPostTask !== undefined) {
1486     this.longTouchAnimationPostTask = setTimeout(animationPostTask, HomeComponent3D.LONG_TOUCH_DURATION_AFTER_DELAY);
1487   }
1488 }
1489 
1490 /**
1491  * @private 
1492  */
1493 HomeComponent3D.prototype.stopLongTouchAnimation = function(x, y) {
1494   if (this.touchOverlay !== undefined) {
1495     this.touchOverlay.style.visibility = "hidden";
1496     for (var i = 0; i < this.touchOverlay.children.length; i++) {
1497       this.touchOverlay.children.item(i).classList.remove("animated");
1498     }
1499     if (this.longTouchAnimationPostTask !== undefined) {
1500       clearTimeout(this.longTouchAnimationPostTask);
1501       delete this.longTouchAnimationPostTask;
1502     }
1503   }
1504 }
1505 
1506 /**
1507  * Installs keys bound to actions. 
1508  * @private 
1509  */
1510 HomeComponent3D.prototype.installKeyboardActions = function() {
1511   // Tolerate alt modifier for forward and backward moves with UP and DOWN keys to avoid 
1512   // the user to release the alt key when he wants to alternate forward/backward and sideways moves
1513   this.inputMap = {
1514       "shift pressed UP" : "MOVE_CAMERA_FAST_FORWARD",
1515       "alt shift pressed UP" : "MOVE_CAMERA_FAST_FORWARD",
1516       "shift pressed W" : "MOVE_CAMERA_FAST_FORWARD",
1517       "pressed UP" : "MOVE_CAMERA_FORWARD",
1518       "alt pressed UP" : "MOVE_CAMERA_FORWARD",
1519       "pressed W" : "MOVE_CAMERA_FORWARD",
1520       "shift pressed DOWN" : "MOVE_CAMERA_FAST_BACKWARD",
1521       "alt shift pressed DOWN" : "MOVE_CAMERA_FAST_BACKWARD",
1522       "shift pressed S" : "MOVE_CAMERA_FAST_BACKWARD",
1523       "pressed DOWN" : "MOVE_CAMERA_BACKWARD",
1524       "alt pressed DOWN" : "MOVE_CAMERA_BACKWARD",
1525       "pressed S" : "MOVE_CAMERA_BACKWARD",
1526       "alt shift pressed LEFT" : "MOVE_CAMERA_FAST_LEFT",
1527       "alt pressed LEFT" : "MOVE_CAMERA_LEFT",
1528       "alt shift pressed RIGHT" : "MOVE_CAMERA_FAST_RIGHT",
1529       "alt pressed RIGHT" : "MOVE_CAMERA_RIGHT",
1530       "shift pressed LEFT" : "ROTATE_CAMERA_YAW_FAST_LEFT",
1531       "shift pressed A" : "ROTATE_CAMERA_YAW_FAST_LEFT",
1532       "pressed LEFT" : "ROTATE_CAMERA_YAW_LEFT",
1533       "pressed A" : "ROTATE_CAMERA_YAW_LEFT",
1534       "shift pressed RIGHT" : "ROTATE_CAMERA_YAW_FAST_RIGHT",
1535       "shift pressed D" : "ROTATE_CAMERA_YAW_FAST_RIGHT",
1536       "pressed RIGHT" : "ROTATE_CAMERA_YAW_RIGHT",
1537       "pressed D" : "ROTATE_CAMERA_YAW_RIGHT",
1538       "shift pressed PAGE_UP" : "ROTATE_CAMERA_PITCH_FAST_UP",
1539       "pressed PAGE_UP" : "ROTATE_CAMERA_PITCH_UP",
1540       "shift pressed PAGE_DOWN" : "ROTATE_CAMERA_PITCH_FAST_DOWN",
1541       "pressed PAGE_DOWN" : "ROTATE_CAMERA_PITCH_DOWN",
1542       "shift pressed HOME" : "ELEVATE_CAMERA_FAST_UP",
1543       "pressed HOME" : "ELEVATE_CAMERA_UP",
1544       "shift pressed END" : "ELEVATE_CAMERA_FAST_DOWN",
1545       "pressed END" : "ELEVATE_CAMERA_DOWN",
1546       "pressed ESCAPE" : "ESCAPE",
1547       "shift pressed ESCAPE" : "ESCAPE"
1548     };
1549     
1550   if (OperatingSystem.isMacOSX()) {
1551     // Under Mac OS X, duplication with Alt key
1552     this.inputMap["alt pressed ALT"] = "ACTIVATE_DUPLICATION";
1553     this.inputMap["released ALT"] = "DEACTIVATE_DUPLICATION";
1554     this.inputMap["shift alt pressed ALT"] = "ACTIVATE_DUPLICATION";
1555     this.inputMap["shift released ALT"] = "DEACTIVATE_DUPLICATION";
1556     this.inputMap["meta alt pressed ALT"] = "ACTIVATE_DUPLICATION";
1557     this.inputMap["meta released ALT"] = "DEACTIVATE_DUPLICATION";
1558     this.inputMap["shift meta alt pressed ALT"] = "ACTIVATE_DUPLICATION";
1559     this.inputMap["shift meta released ALT"] = "DEACTIVATE_DUPLICATION";
1560     this.inputMap["alt pressed ESCAPE"] = "ESCAPE";
1561   } else {
1562     // Under other systems, duplication with Ctrl key
1563     this.inputMap["control pressed CONTROL"] = "ACTIVATE_DUPLICATION";
1564     this.inputMap["released CONTROL"] = "DEACTIVATE_DUPLICATION";
1565     this.inputMap["shift control pressed CONTROL"] = "ACTIVATE_DUPLICATION";
1566     this.inputMap["shift released CONTROL"] = "DEACTIVATE_DUPLICATION";
1567     this.inputMap["meta control pressed CONTROL"] = "ACTIVATE_DUPLICATION";
1568     this.inputMap["meta released CONTROL"] = "DEACTIVATE_DUPLICATION";
1569     this.inputMap["shift meta control pressed CONTROL"] = "ACTIVATE_DUPLICATION";
1570     this.inputMap["shift meta released CONTROL"] = "DEACTIVATE_DUPLICATION";
1571     this.inputMap["control pressed ESCAPE"] = "ESCAPE";
1572   }
1573   if (OperatingSystem.isWindows()) {
1574     // Under Windows, magnetism toggled with Alt key
1575     this.inputMap["alt pressed ALT"] = "TOGGLE_MAGNETISM_ON";
1576     this.inputMap["released ALT"] = "TOGGLE_MAGNETISM_OFF";
1577     this.inputMap["shift alt pressed ALT"] = "TOGGLE_MAGNETISM_ON";
1578     this.inputMap["shift released ALT"] = "TOGGLE_MAGNETISM_OFF";
1579     this.inputMap["control alt pressed ALT"] = "TOGGLE_MAGNETISM_ON";
1580     this.inputMap["control released ALT"] = "TOGGLE_MAGNETISM_OFF";
1581     this.inputMap["shift control alt pressed ALT"] = "TOGGLE_MAGNETISM_ON";
1582     this.inputMap["shift control released ALT"] = "TOGGLE_MAGNETISM_OFF";
1583     this.inputMap["alt pressed ESCAPE"] = "ESCAPE";
1584   } else if (OperatingSystem.isMacOSX()) {
1585     // Under Mac OS X, magnetism toggled with cmd key
1586     this.inputMap["meta pressed META"] = "TOGGLE_MAGNETISM_ON";
1587     this.inputMap["released META"] = "TOGGLE_MAGNETISM_OFF";
1588     this.inputMap["shift meta pressed META"] = "TOGGLE_MAGNETISM_ON";
1589     this.inputMap["shift released META"] = "TOGGLE_MAGNETISM_OFF";
1590     this.inputMap["alt meta pressed META"] = "TOGGLE_MAGNETISM_ON";
1591     this.inputMap["alt released META"] = "TOGGLE_MAGNETISM_OFF";
1592     this.inputMap["shift alt meta pressed META"] = "TOGGLE_MAGNETISM_ON";
1593     this.inputMap["shift alt released META"] = "TOGGLE_MAGNETISM_OFF";
1594     this.inputMap["meta pressed ESCAPE"] = "ESCAPE";
1595   } else {
1596     // Under other Unix systems, magnetism toggled with Alt + Shift key
1597     this.inputMap["shift alt pressed ALT"] = "TOGGLE_MAGNETISM_ON";
1598     this.inputMap["alt shift pressed SHIFT"] = "TOGGLE_MAGNETISM_ON";
1599     this.inputMap["alt released SHIFT"] = "TOGGLE_MAGNETISM_OFF";
1600     this.inputMap["shift released ALT"] = "TOGGLE_MAGNETISM_OFF";
1601     this.inputMap["control shift alt pressed ALT"] = "TOGGLE_MAGNETISM_ON";
1602     this.inputMap["control alt shift pressed SHIFT"] = "TOGGLE_MAGNETISM_ON";
1603     this.inputMap["control alt released SHIFT"] = "TOGGLE_MAGNETISM_OFF";
1604     this.inputMap["control shift released ALT"] = "TOGGLE_MAGNETISM_OFF";
1605     this.inputMap["alt shift ESCAPE"] = "ESCAPE";
1606     this.inputMap["control alt shift pressed ESCAPE"] = "ESCAPE";
1607   }
1608 
1609   this.inputMap["shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT";
1610   this.inputMap["released SHIFT"] = "DEACTIVATE_ALIGNMENT";
1611   if (OperatingSystem.isWindows()) {
1612     this.inputMap["control shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT";
1613     this.inputMap["control released SHIFT"] = "DEACTIVATE_ALIGNMENT";
1614     this.inputMap["alt shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT";
1615     this.inputMap["alt released SHIFT"] = "DEACTIVATE_ALIGNMENT";
1616   } else if (OperatingSystem.isMacOSX()) {
1617     this.inputMap["alt shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT";
1618     this.inputMap["alt released SHIFT"] = "DEACTIVATE_ALIGNMENT";
1619     this.inputMap["meta shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT";
1620     this.inputMap["meta released SHIFT"] = "DEACTIVATE_ALIGNMENT";
1621   } else {
1622     this.inputMap["control shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT";
1623     this.inputMap["control released SHIFT"] = "DEACTIVATE_ALIGNMENT";
1624     this.inputMap["shift released ALT"] = "ACTIVATE_ALIGNMENT";
1625     this.inputMap["control shift released ALT"] = "ACTIVATE_ALIGNMENT";
1626   }
1627   
1628   var component3D = this;
1629   this.canvas3D.getHTMLElement().addEventListener("keydown", 
1630       function(ev) {
1631         component3D.callAction(ev, KeyStroke.getKeyStrokeForEvent(ev, "keydown"));
1632       }, false);
1633   this.canvas3D.getHTMLElement().addEventListener("keyup", 
1634       function(ev) {
1635         component3D.callAction(ev, KeyStroke.getKeyStrokeForEvent(ev, "keyup"));
1636       }, false);
1637 }
1638 
1639 /**
1640  * Runs the action bound to the key stroke in parameter.
1641  * @param {UIEvent} ev
1642  * @param {string} keyStroke
1643  * @private 
1644  */
1645 HomeComponent3D.prototype.callAction = function(ev, keyStroke) {
1646   if (keyStroke !== undefined) {
1647     var actionKey = this.inputMap [keyStroke];
1648     if (actionKey !== undefined) {
1649       var action = this.actionMap [actionKey];
1650       if (action !== undefined) {
1651         action.actionPerformed(ev);
1652       }
1653       ev.stopPropagation();
1654     }
1655   }
1656 }
1657 
1658 /**
1659  * Creates actions that calls back <code>controller</code> methods.  
1660  * @private 
1661  */
1662 HomeComponent3D.prototype.createActions = function(controller) {
1663   // Move camera action mapped to arrow keys.
1664   function MoveCameraAction(delta) {
1665     this.delta = delta;
1666   }
1667 
1668   MoveCameraAction.prototype.actionPerformed = function(ev) {
1669     controller.moveCamera(this.delta);
1670   }
1671 
1672   // Move camera sideways action mapped to arrow keys.
1673   function MoveCameraSidewaysAction (delta) {
1674     this.delta = delta;
1675   }
1676 
1677   MoveCameraSidewaysAction.prototype.actionPerformed = function(ev) {
1678     controller.moveCameraSideways(this.delta);
1679   }
1680 
1681   // Elevate camera action mapped to arrow keys.
1682   function ElevateCameraAction(delta) {
1683     this.delta = delta;
1684   }
1685 
1686   ElevateCameraAction.prototype.actionPerformed = function(ev) {
1687     controller.elevateCamera(this.delta);
1688   }
1689 
1690   // Rotate camera yaw action mapped to arrow keys.
1691   function RotateCameraYawAction (delta) {
1692     this.delta = delta;
1693   }
1694 
1695   RotateCameraYawAction.prototype.actionPerformed = function(ev) {
1696     controller.rotateCameraYaw(this.delta);
1697   }
1698 
1699   // Rotate camera pitch action mapped to arrow keys.
1700   function RotateCameraPitchAction(delta) {
1701     this.delta = delta;
1702   }
1703 
1704   RotateCameraPitchAction.prototype.actionPerformed = function(ev) {
1705     controller.rotateCameraPitch(this.delta);
1706   }
1707 
1708   this.actionMap = {
1709       "MOVE_CAMERA_FORWARD" : new MoveCameraAction(6.5),
1710       "MOVE_CAMERA_FAST_FORWARD" : new MoveCameraAction(32.5),
1711       "MOVE_CAMERA_BACKWARD" : new MoveCameraAction(-6.5),
1712       "MOVE_CAMERA_FAST_BACKWARD" : new MoveCameraAction(-32.5),
1713       "MOVE_CAMERA_LEFT" : new MoveCameraSidewaysAction(-2.5),
1714       "MOVE_CAMERA_FAST_LEFT" : new MoveCameraSidewaysAction(-10),
1715       "MOVE_CAMERA_RIGHT" : new MoveCameraSidewaysAction(2.5),
1716       "MOVE_CAMERA_FAST_RIGHT" : new MoveCameraSidewaysAction(10),
1717       "ELEVATE_CAMERA_DOWN" : new ElevateCameraAction(-2.5),
1718       "ELEVATE_CAMERA_FAST_DOWN" : new ElevateCameraAction(-10),
1719       "ELEVATE_CAMERA_UP" : new ElevateCameraAction(2.5),
1720       "ELEVATE_CAMERA_FAST_UP" : new ElevateCameraAction(10),
1721       "ROTATE_CAMERA_YAW_LEFT" : new RotateCameraYawAction(-Math.PI / 60),
1722       "ROTATE_CAMERA_YAW_FAST_LEFT" : new RotateCameraYawAction(-Math.PI / 12),
1723       "ROTATE_CAMERA_YAW_RIGHT" : new RotateCameraYawAction(Math.PI / 60),
1724       "ROTATE_CAMERA_YAW_FAST_RIGHT" : new RotateCameraYawAction(Math.PI / 12),
1725       "ROTATE_CAMERA_PITCH_UP" : new RotateCameraPitchAction(-Math.PI / 120),
1726       "ROTATE_CAMERA_PITCH_FAST_UP" : new RotateCameraPitchAction(-Math.PI / 24),
1727       "ROTATE_CAMERA_PITCH_DOWN" : new RotateCameraPitchAction(Math.PI / 120),
1728       "ROTATE_CAMERA_PITCH_FAST_DOWN" : new RotateCameraPitchAction(Math.PI / 24),
1729       "ESCAPE": {
1730           actionPerformed: function(ev) {
1731             controller.escape();
1732           }
1733         },
1734       "ACTIVATE_ALIGNMENT": {
1735           actionPerformed: function(ev) {
1736             controller.setAlignmentActivated(true);
1737           }
1738         },
1739       "DEACTIVATE_ALIGNMENT": {
1740           actionPerformed: function(ev) {
1741             controller.setAlignmentActivated(false);
1742           }
1743         },
1744       "TOGGLE_MAGNETISM_ON": {
1745           actionPerformed: function(ev) {
1746             controller.toggleMagnetism(true);
1747           }
1748         },
1749       "TOGGLE_MAGNETISM_OFF": {
1750           actionPerformed: function(ev) {
1751             controller.toggleMagnetism(false);
1752           }
1753         },
1754       "ACTIVATE_DUPLICATION": {
1755           actionPerformed: function(ev) {
1756             controller.setDuplicationActivated(true);
1757           }
1758         },
1759       "DEACTIVATE_DUPLICATION": {
1760           actionPerformed: function(ev) {
1761             controller.setDuplicationActivated(false);
1762           }
1763         }
1764     };
1765 }
1766 
1767 /**
1768  * Returns the action map of this component.
1769  */
1770 HomeComponent3D.prototype.getActionMap = function() {
1771   return this.actionMap;
1772 }
1773 
1774 /**
1775  * Returns the input map of this component.
1776  */
1777 HomeComponent3D.prototype.getInputMap = function() {
1778   return this.inputMap;
1779 }
1780 
1781 /**
1782  * Returns the closest home item displayed at client coordinates (x, y). 
1783  * @param {number} x
1784  * @param {number} y
1785  * @return {Object}
1786  * @deprecated
1787  */
1788 HomeComponent3D.prototype.getClosestItemAt = function(x, y) {
1789   var node = this.canvas3D.getClosestShapeAt(x, y);
1790   var homeObjectIndex = -1;
1791   while (node !== null
1792          && (homeObjectIndex = this.homeObjects3D.indexOf(node)) < 0) {
1793     node = node.getParent();
1794   }
1795   if (node != null) {
1796     return this.homeObjects [homeObjectIndex];
1797   } else {
1798     return null;
1799   }
1800 }
1801 
1802 /**
1803  * Returns the closest home item displayed at component coordinates (x, y),
1804  * or <code>null</code> if not found. 
1805  * @param {number} x
1806  * @param {number} y
1807  * @return {Selectable}
1808  */
1809 HomeComponent3D.prototype.getClosestSelectableItemAt = function(x, y) {
1810   var rect = component3D.canvas3D.getHTMLElement().getBoundingClientRect();
1811   return this.getClosestItemAt(x + rect.left, y + rect.top);
1812 }
1813 
1814 /**
1815  * Returns the 3D point matching the point (x, y) in component coordinates space.
1816  * @return {vec3}
1817  * @private 
1818  */
1819 HomeComponent3D.prototype.convertPixelLocationToVirtualWorldPoint = function(x, y) {
1820   // See http://webglfactory.blogspot.com/2011/05/how-to-convert-world-to-screen.html
1821   var transform = this.canvas3D.getVirtualWorldToImageTransform(mat4.create());
1822   mat4.invert(transform, transform);
1823   mat4.mul(transform, this.canvas3D.getViewPlatformTransform(mat4.create()), transform);
1824   
1825   var rect = this.getHTMLElement().getBoundingClientRect();
1826   var point = vec3.fromValues((x / rect.width - 0.5) * 2, (0.5 - y / rect.height) * 2, 0);
1827   vec3.transformMat4(point, point, transform);
1828   return point;
1829 }
1830 
1831 /**
1832  * Returns the 3D point matching the point (x, y) in component coordinates space.
1833  * @return {Array}
1834  */
1835 HomeComponent3D.prototype.convertPixelLocationToVirtualWorld = function(x, y) {
1836   var point = this.convertPixelLocationToVirtualWorldPoint(x, y);
1837   return [point [0], point [2], point [1]];
1838 }
1839 
1840 /**
1841  * Returns the coordinates intersecting the floor of the selected level in the direction
1842  * joining camera location and component coordinates (x, y).
1843  */
1844 HomeComponent3D.prototype.getVirtualWorldPointAt = function(x, y, elevation) {
1845   var point = this.convertPixelLocationToVirtualWorldPoint(x, y);
1846   var camera = this.home.getCamera();
1847   var eye = vec3.fromValues(camera.getX(), camera.getZ(), camera.getY());
1848   var eyePointDirection = vec3.sub(vec3.fromValues(0, 0, 0), point, eye);
1849   // If direction points to the sky, negate it to point to the ground
1850   if (eyePointDirection [1] > 0) {
1851     eyePointDirection [1] = -eyePointDirection [1];
1852   }
1853 
1854   // Compute coordinates of the intersection point between the line joining
1855   // eye and the given point with the plan y = elevation
1856   // Parametric equation of the line
1857   // x = point.x + t . direction.x
1858   // y = point.y + t . direction.y
1859   // z = point.z + t . direction.z
1860   var t = (elevation - point [1]) / eyePointDirection [1];
1861   var xFloor = (point [0] + t * eyePointDirection [0]);
1862   var zFloor = (point [2] + t * eyePointDirection [2]);
1863   return [xFloor, zFloor, elevation];
1864 }
1865 
1866 /**
1867  * Returns a new scene tree root.
1868  * @private 
1869  */
1870 HomeComponent3D.prototype.createSceneTree = function(listenToHomeUpdates, waitForLoading) {
1871   var root = new Group3D();
1872   // Build scene tree with background node first to ensure home structure will be loaded first if it exists
1873   root.addChild(this.createBackgroundNode(listenToHomeUpdates, waitForLoading));
1874   // Limit ground area to 1 km x 1 km to avoid bad effects with a larger area 
1875   var groundNode = this.createGroundNode(-0.5E5, -0.5E5, 1E5, 1E5, listenToHomeUpdates, waitForLoading);
1876   root.addChild(groundNode);
1877   root.addChild(this.createHomeTree(listenToHomeUpdates, waitForLoading)); 
1878   
1879   this.sceneLights = this.createLights(listenToHomeUpdates);
1880   for (var i = 0; i < this.sceneLights.length; i++) {
1881     root.addChild(this.sceneLights [i]);
1882   }
1883   
1884   return root;
1885 }
1886 
1887 /**
1888  * Returns a new background node.  
1889  * @private 
1890  */
1891 HomeComponent3D.prototype.createBackgroundNode = function(listenToHomeUpdates, waitForLoading) {
1892   var skyBackgroundAppearance = new Appearance3D();
1893   var topHalfSphereGeometry = this.createHalfSphereGeometry(true);   
1894   var topHalfSphere = new Shape3D(topHalfSphereGeometry, skyBackgroundAppearance);
1895   var backgroundGroup = new BranchGroup3D();
1896   backgroundGroup.addChild(topHalfSphere);
1897   backgroundGroup.addChild(new Shape3D(this.createHalfSphereGeometry(false)));
1898 
1899   // Add a plane at ground level to complete landscape at the horizon when camera is above horizon 
1900   var groundBackgroundAppearance = new Appearance3D();
1901   var groundBackground = new Shape3D( 
1902       new IndexedTriangleArray3D([vec3.fromValues(-1, -0.01, -1),
1903                                   vec3.fromValues(-1, -0.01, 1),
1904                                   vec3.fromValues(1, -0.01, 1),
1905                                   vec3.fromValues(1, -0.01, -1)],
1906                                  [0, 1, 2, 0, 2, 3],
1907                                  [], [],
1908                                  [vec3.fromValues(0., 1., 0.)], [0, 0, 0, 0, 0, 0]),
1909       groundBackgroundAppearance);
1910   backgroundGroup.addChild(groundBackground);
1911   
1912   // No need of different lights for background because scene lights will have an effect on background too
1913   
1914   var background = new Background3D(backgroundGroup);
1915   this.updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, this.home, waitForLoading);
1916   groundBackgroundAppearance.setVisible(this.home.getCamera().getZ() >= 0);
1917 
1918   if (listenToHomeUpdates) {
1919     // Add a listener on home properties change 
1920     var component3D = this;
1921     this.backgroundChangeListener = function(ev) {
1922         component3D.updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, 
1923             component3D.home, waitForLoading);
1924       };
1925     component3D.home.getEnvironment().addPropertyChangeListener("SKY_COLOR", this.backgroundChangeListener);
1926     component3D.home.getEnvironment().addPropertyChangeListener("SKY_TEXTURE", this.backgroundChangeListener);
1927     component3D.home.getEnvironment().addPropertyChangeListener("GROUND_COLOR", this.backgroundChangeListener);
1928     component3D.home.getEnvironment().addPropertyChangeListener("GROUND_TEXTURE", this.backgroundChangeListener);
1929     // Make groundBackground invisible if camera is below the ground
1930     this.elevationChangeListener = function(ev) {
1931         if (ev.getSource() === component3D.home) {
1932           // Move listener to the new camera
1933           ev.getOldValue().removePropertyChangeListener(component3D.elevationChangeListener);
1934           component3D.home.getCamera().addPropertyChangeListener(component3D.elevationChangeListener);
1935         } 
1936         if (ev.getSource() === component3D.home
1937             || ev.getPropertyName() === "Z") {
1938           groundBackgroundAppearance.setVisible(component3D.home.getCamera().getZ() >= 0);
1939         }
1940       };
1941     this.home.getCamera().addPropertyChangeListener(this.elevationChangeListener);
1942     this.home.addPropertyChangeListener("CAMERA", this.elevationChangeListener);
1943   }
1944   return background;
1945 }
1946 
1947 /**
1948  * Returns a half sphere oriented inward and with texture ordinates 
1949  * that spread along an hemisphere. 
1950  * @param {boolean} top  if true returns an upper geometry
1951  * @private 
1952  */
1953 HomeComponent3D.prototype.createHalfSphereGeometry = function(top) {
1954   var divisionCount = 48; 
1955   var coords = [];
1956   var coordIndices = [];
1957   var textureCoords = [];
1958   for (var i = 0, k = 0; i < divisionCount; i++) {
1959     var alpha = i * 2 * Math.PI / divisionCount;
1960     var cosAlpha = Math.cos(alpha);
1961     var sinAlpha = Math.sin(alpha);
1962     var nextAlpha = (i  + 1) * 2 * Math.PI / divisionCount;
1963     var cosNextAlpha = Math.cos(nextAlpha);
1964     var sinNextAlpha = Math.sin(nextAlpha);
1965     for (var j = 0, max = divisionCount / 4; j < max; j++, k += 4) {
1966       var beta = 2 * j * Math.PI / divisionCount;
1967       var cosBeta = Math.cos(beta); 
1968       var sinBeta = Math.sin(beta);
1969       // Correct the bottom of the hemisphere to avoid seeing a bottom hemisphere at the horizon
1970       var y = j !== 0 ? (top ? sinBeta : -sinBeta) : -0.01;
1971       var nextBeta = 2 * (j + 1) * Math.PI / divisionCount;
1972       if (!top) {
1973         nextBeta = -nextBeta;
1974       }
1975       var cosNextBeta = Math.cos(nextBeta);
1976       var sinNextBeta = Math.sin(nextBeta);
1977       coords.push(vec3.fromValues(cosAlpha * cosBeta, y, sinAlpha * cosBeta));
1978       coords.push(vec3.fromValues(cosNextAlpha * cosBeta, y, sinNextAlpha * cosBeta));
1979       coords.push(vec3.fromValues(cosNextAlpha * cosNextBeta, sinNextBeta, sinNextAlpha * cosNextBeta));
1980       coords.push(vec3.fromValues(cosAlpha * cosNextBeta, sinNextBeta, sinAlpha * cosNextBeta));
1981       if (top) {
1982         coordIndices.push(k);
1983         coordIndices.push(k + 1);
1984         coordIndices.push(k + 2);
1985         coordIndices.push(k);
1986         coordIndices.push(k + 2);
1987         coordIndices.push(k + 3);
1988         textureCoords.push(vec2.fromValues(i / divisionCount, j / max)); 
1989         textureCoords.push(vec2.fromValues((i + 1) / divisionCount, j / max)); 
1990         textureCoords.push(vec2.fromValues((i + 1) / divisionCount, (j + 1) / max)); 
1991         textureCoords.push(vec2.fromValues(i / divisionCount, (j + 1) / max));
1992       } else {
1993         coordIndices.push(k);
1994         coordIndices.push(k + 2);
1995         coordIndices.push(k + 1);
1996         coordIndices.push(k);
1997         coordIndices.push(k + 3);
1998         coordIndices.push(k + 2);
1999       }
2000     }
2001   }
2002   
2003   return new IndexedTriangleArray3D(coords, coordIndices, textureCoords, coordIndices, [], []);
2004 }
2005 
2006 /**
2007  * Updates <code>skyBackgroundAppearance</code> and <code>groundBackgroundAppearance</code> 
2008  * color / texture from <code>home</code> sky color and texture.
2009  * @param {Appearance3D} skyBackgroundAppearance    the sky appearance to update
2010  * @param {Appearance3D} groundBackgroundAppearance the shape of the ground used in the background     
2011  * @param {Home}         home
2012  * @param {boolean}      waitForLoading
2013  * @private 
2014  */
2015 HomeComponent3D.prototype.updateBackgroundColorAndTexture = function(skyBackgroundAppearance, groundBackgroundAppearance, 
2016                                                                      home, waitForLoading) {
2017   var skyColor = home.getEnvironment().getSkyColor();
2018   skyBackgroundAppearance.setDiffuseColor(vec3.fromValues(((skyColor >>> 16) & 0xFF) / 255.,
2019                                                           ((skyColor >>> 8) & 0xFF) / 255.,
2020                                                            (skyColor & 0xFF) / 255.));
2021   var skyTexture = home.getEnvironment().getSkyTexture();
2022   if (skyTexture !== null) {
2023     var transform = mat3.create();
2024     mat3.fromTranslation(transform, vec2.fromValues(-skyTexture.getXOffset(), 0));
2025     TextureManager.getInstance().loadTexture(skyTexture.getImage(), 0, waitForLoading,
2026         {
2027           textureUpdated : function(textureImage) {
2028             skyBackgroundAppearance.setTextureImage(textureImage);
2029             skyBackgroundAppearance.setTextureTransform(transform);
2030           },
2031           textureError : function(error) {
2032             return this.textureUpdated(TextureManager.getInstance().getErrorImage());
2033           } 
2034         });
2035   } else {
2036     skyBackgroundAppearance.setTextureImage(null);
2037   }
2038   
2039   var groundColor = home.getEnvironment().getGroundColor();
2040   var color = vec3.fromValues(((groundColor >>> 16) & 0xFF) / 255.,
2041                               ((groundColor >>> 8) & 0xFF) / 255.,
2042                                (groundColor & 0xFF) / 255.);
2043   groundBackgroundAppearance.setDiffuseColor(color);
2044   groundBackgroundAppearance.setAmbientColor(color);
2045   var groundTexture = home.getEnvironment().getGroundTexture();
2046   if (groundTexture !== null) {
2047     TextureManager.getInstance().loadTexture(groundTexture.getImage(), 0, waitForLoading,
2048         {
2049           textureUpdated : function(textureImage) {
2050             // Display texture very small to get an average color at the horizon 
2051             groundBackgroundAppearance.setTextureImage(textureImage);
2052             groundBackgroundAppearance.setTextureCoordinatesGeneration(
2053                 {planeS : vec4.fromValues(1E5, 0, 0, 0), 
2054                  planeT : vec4.fromValues(0, 0, 1E5, 0)});
2055           },
2056           textureError : function(error) {
2057             return this.textureUpdated(TextureManager.getInstance().getErrorImage());
2058           }
2059         });
2060   } else {
2061     groundBackgroundAppearance.setTextureImage(null);
2062   }
2063 }
2064 
2065 /**
2066  * Returns a new ground node.  
2067  * @private 
2068  */
2069 HomeComponent3D.prototype.createGroundNode = function(groundOriginX, groundOriginY, groundWidth, groundDepth, 
2070                                                       listenToHomeUpdates, waitForLoading) {
2071   if (this.home.structure) {
2072     var structureGroup = new BranchGroup3D();
2073     structureGroup.setCapability(Group3D.ALLOW_CHILDREN_EXTEND);
2074     ModelManager.getInstance().loadModel(this.home.structure, waitForLoading,
2075         { 
2076           modelUpdated : function(structureNode) {
2077             structureGroup.addChild(structureNode);
2078           },
2079           modelError : function(ex) {
2080             // Display a large red box at ground level
2081             var boxAppearance = new Appearance3D();
2082             boxAppearance.setDiffuseColor(vec3.fromValues(1, 0, 0));
2083             structureGroup.addChild(new Box3D(1E7, 0, 1E7, boxAppearance));
2084           }
2085         });
2086     
2087     this.groundChangeListener = function(ev) {}; // Dummy listener
2088     return structureGroup;
2089   } else {
2090     var ground3D = typeof Ground3D !== "undefined" 
2091         ? new Ground3D(this.home, groundOriginX, groundOriginY, groundWidth, groundDepth, waitForLoading) 
2092         : new Box3D(1E7, 0, 1E7, new Appearance3D());
2093     var translation = mat4.create();
2094     mat4.translate(translation, translation, vec3.fromValues(0, -0.2, 0));
2095     var transformGroup = new TransformGroup3D(translation);
2096     transformGroup.addChild(ground3D);
2097 
2098     if (listenToHomeUpdates) {
2099       var component3D = this;
2100       // Add a listener on ground color and texture properties change
2101       this.groundChangeListener = function(ev) {
2102           if (!component3D.groundChangeListener.updater) {
2103             component3D.groundChangeListener.updater = function() {
2104                 ground3D.update();
2105                 delete component3D.groundChangeListener.updater;
2106               };
2107             setTimeout(component3D.groundChangeListener.updater, 0);
2108           }
2109         };
2110       var homeEnvironment = this.home.getEnvironment();
2111       homeEnvironment.addPropertyChangeListener("GROUND_COLOR", this.groundChangeListener);
2112       homeEnvironment.addPropertyChangeListener("BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D", this.groundChangeListener);
2113       homeEnvironment.addPropertyChangeListener("GROUND_TEXTURE", this.groundChangeListener);
2114       this.home.addPropertyChangeListener("BACKGROUND_IMAGE", this.groundChangeListener);
2115     }    
2116     return transformGroup;
2117   }
2118 }
2119 
2120 /**
2121  * Returns the lights of the scene.
2122  * @private 
2123  */
2124 HomeComponent3D.prototype.createLights = function(listenToHomeUpdates) {
2125   var lights = [
2126       new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(1.5, -0.8, -1)),         
2127       new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(-1.5, -0.8, -1)), 
2128       new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(0, -0.8, 1)), 
2129       new DirectionalLight3D(vec3.fromValues(0.7, 0.7, 0.7), vec3.fromValues(0, 1, 0)), 
2130       new AmbientLight3D(vec3.fromValues(0.2, 0.2, 0.2))]; 
2131   for (var i = 0; i < lights.length - 1; i++) {
2132     // Store default color 
2133     lights [i].defaultColor = lights [i].getColor();
2134     this.updateLightColor(lights [i]);
2135   }
2136   
2137   if (listenToHomeUpdates) {
2138     // Add a listener on light color property change to home
2139     var component3D = this;
2140     this.lightColorListener = function(ev) {
2141         for (var i = 0; i < lights.length - 1; i++) {
2142           component3D.updateLightColor(lights [i]);
2143         }
2144       };
2145     this.home.getEnvironment().addPropertyChangeListener(
2146         "LIGHT_COLOR", this.lightColorListener);
2147   }
2148 
2149   return lights;
2150 }
2151 
2152 /**
2153  * Updates<code>light</code> color from <code>home</code> light color.
2154  * @param {Light3D} light the light to update 
2155  * @private 
2156  */
2157 HomeComponent3D.prototype.updateLightColor = function(light) {
2158   var defaultColor = light.defaultColor;
2159   var lightColor = this.home.getEnvironment().getLightColor();
2160   light.setColor(vec3.fromValues(((lightColor >>> 16) & 0xFF) / 255 * defaultColor [0],
2161                                   ((lightColor >>> 8) & 0xFF) / 255 * defaultColor [1],
2162                                           (lightColor & 0xFF) / 255 * defaultColor [2]));
2163 }
2164 
2165 /**
2166  * Returns a <code>home</code> new tree node, with branches for each wall 
2167  * and piece of furniture of <code>home</code>. 
2168  * @private 
2169  */
2170 HomeComponent3D.prototype.createHomeTree = function(listenToHomeUpdates, waitForLoading) {
2171   var homeRoot = new BranchGroup3D();
2172   homeRoot.setCapability(Group3D.ALLOW_CHILDREN_EXTEND);
2173   // Add walls, pieces, rooms, polylines, dimension lines and labels already available
2174   var labels = this.home.getLabels();
2175   for (var i = 0; i < labels.length; i++) {
2176     this.addObject(homeRoot, labels [i], listenToHomeUpdates, waitForLoading);
2177   }
2178   var dimensionLines = this.home.getDimensionLines();
2179   for (var i = 0; i < dimensionLines.length; i++) {
2180     this.addObject(homeRoot, dimensionLines [i], listenToHomeUpdates, waitForLoading);
2181   }
2182   var polylines = this.home.getPolylines();
2183   for (var i = 0; i < polylines.length; i++) {
2184     this.addObject(homeRoot, polylines [i], listenToHomeUpdates, waitForLoading);
2185   }
2186   var rooms = this.home.getRooms();
2187   for (var i = 0; i < rooms.length; i++) {
2188     this.addObject(homeRoot, rooms [i], listenToHomeUpdates, waitForLoading);
2189   }    
2190   var walls = this.home.getWalls();
2191   for (var i = 0; i < walls.length; i++) {
2192     this.addObject(homeRoot, walls [i], listenToHomeUpdates, waitForLoading);
2193   }
2194   var furniture = this.home.getFurniture();
2195   for (var i = 0; i < furniture.length; i++) { 
2196     var piece = furniture [i];
2197     if (piece instanceof HomeFurnitureGroup) {
2198       var groupFurniture = piece.getAllFurniture();
2199       for (var j = 0; j < groupFurniture.length; j++) {
2200         var childPiece = groupFurniture [j];
2201         if (!(childPiece instanceof HomeFurnitureGroup)) {
2202           this.addObject(homeRoot, childPiece, listenToHomeUpdates, waitForLoading);
2203         }
2204       }
2205     } else {
2206       this.addObject(homeRoot, piece, listenToHomeUpdates, waitForLoading);
2207     }
2208   }
2209   if (listenToHomeUpdates) {
2210     // Add level, wall, furniture, room listeners to home for further update    
2211     this.addLevelListener(homeRoot);
2212     this.addWallListener(homeRoot);
2213     this.addFurnitureListener(homeRoot);
2214     this.addRoomListener(homeRoot);
2215     this.addPolylineListener(homeRoot);
2216     this.addDimensionLineListener(homeRoot);
2217     this.addLabelListener(homeRoot);
2218     this.addEnvironmentListeners();
2219     component3D = this;
2220     this.selectionListener = {
2221        selectionChanged: function(ev) {
2222           component3D.updateObjectsAndFurnitureGroups(ev.getOldSelectedItems());
2223           component3D.updateObjectsAndFurnitureGroups(ev.getSelectedItems());
2224         }
2225       };
2226     this.home.addSelectionListener(this.selectionListener);
2227   }
2228   return homeRoot;
2229 }
2230 
2231 /**
2232  * Adds a level listener to home levels that updates the children of the given
2233  * <code>group</code>, each time a level is added, updated or deleted.
2234  * @param {Group3D} group
2235  * @private
2236  */
2237 HomeComponent3D.prototype.addLevelListener = function(group) {
2238   var component3D = this;
2239   this.levelChangeListener = function(ev) {
2240       var propertyName = ev.getPropertyName();
2241       if ("VISIBLE" == propertyName
2242           || "VIEWABLE" == propertyName) {
2243         var objects = component3D.homeObjects;
2244         var updatedItems = [];
2245         for (var i = 0; i < objects.length; i++) {
2246           var item = objects [i];
2247           if (item instanceof Room // 3D rooms depend on rooms at other levels
2248               || item.isAtLevel !== undefined // item instanceof Elevetable
2249               || item.isAtLevel(ev.getSource())) {
2250             updatedItems.push(item);
2251           }
2252         }
2253         component3D.updateObjects(updatedItems);          
2254         component3D.groundChangeListener(null);
2255       } else if ("ELEVATION" == propertyName) {
2256         component3D.updateObjects(component3D.homeObjects.slice(0));          
2257         component3D.groundChangeListener(null);
2258       } else if ("BACKGROUND_IMAGE" == propertyName) {
2259         component3D.groundChangeListener(null);
2260       } else if ("FLOOR_THICKNESS" == propertyName) {
2261         component3D.updateObjects(component3D.home.getWalls());          
2262         component3D.updateObjects(component3D.home.getRooms());
2263       } else if ("HEIGHT" == propertyName) {
2264         component3D.updateObjects(component3D.home.getRooms());
2265       }  
2266     };
2267   var levels = this.home.getLevels();
2268   for (var i = 0; i < levels.length; i++) {
2269     levels[i].addPropertyChangeListener(this.levelChangeListener);
2270   }
2271 
2272   this.levelListener = function(ev) {
2273       var level = ev.getItem();
2274       switch ((ev.getType())) {
2275         case CollectionEvent.Type.ADD :
2276           level.addPropertyChangeListener(component3D.levelChangeListener);
2277           break;
2278         case CollectionEvent.Type.DELETE :
2279           level.removePropertyChangeListener(component3D.levelChangeListener);
2280           break;
2281         }
2282       component3D.updateObjects(component3D.home.getRooms());
2283     };
2284   this.home.addLevelsListener(this.levelListener);
2285 }
2286 
2287 /**
2288  * Adds a wall listener to home walls that updates the children of the given
2289  * <code>group</code>, each time a wall is added, updated or deleted.
2290  * @param {Group3D} group
2291  * @private
2292  */
2293 HomeComponent3D.prototype.addWallListener = function(group) {
2294   var component3D = this;
2295   this.wallChangeListener = function(ev) {
2296       var propertyName = ev.getPropertyName();
2297       if ("PATTERN" != propertyName) {
2298         var updatedWall = ev.getSource();
2299         component3D.updateWall(updatedWall);          
2300         var levels = component3D.home.getLevels();
2301         if (updatedWall.getLevel() === null
2302             || updatedWall.isAtLevel(levels [levels.length - 1])) {
2303           component3D.updateObjects(component3D.home.getRooms());
2304         }
2305         if (updatedWall.getLevel() != null && updatedWall.getLevel().getElevation() < 0) {
2306           component3D.groundChangeListener(null);
2307         }
2308       }
2309     };
2310   var walls = this.home.getWalls();
2311   for (var i = 0; i < walls.length; i++) {
2312     walls[i].addPropertyChangeListener(this.wallChangeListener);
2313   }
2314   this.wallListener = function(ev) {
2315       var wall = ev.getItem();
2316       switch ((ev.getType())) {
2317         case CollectionEvent.Type.ADD :
2318           component3D.addObject(group, wall, true, false);
2319           wall.addPropertyChangeListener(component3D.wallChangeListener);
2320           break;
2321         case CollectionEvent.Type.DELETE :
2322           component3D.deleteObject(wall);
2323           wall.removePropertyChangeListener(component3D.wallChangeListener);
2324           break;
2325       }
2326       component3D.updateObjects(component3D.home.getRooms());
2327       component3D.groundChangeListener(null);
2328     };
2329   this.home.addWallsListener(this.wallListener);
2330 }
2331 
2332 /**
2333  * Adds a furniture listener to home that updates the children of the given <code>group</code>, 
2334  * each time a piece of furniture is added, updated or deleted.
2335  * @private 
2336  */
2337 HomeComponent3D.prototype.addFurnitureListener = function(group) {
2338   var component3D = this;
2339   var updatePieceOfFurnitureGeometry = function(piece, propertyName, oldValue) {
2340       component3D.updateObjects([piece]);
2341       if (component3D.containsDoorsAndWindows(piece)) {
2342         if (oldValue !== null) {
2343           var oldPiece = piece.clone();
2344           if ("X" == propertyName) {
2345             oldPiece.setX(oldValue);
2346           } else if ("Y" == propertyName) {
2347             oldPiece.setY(oldValue);
2348           } else if ("ANGLE" == propertyName) {
2349             oldPiece.setAngle(oldValue);
2350           } else if ("WIDTH" == propertyName) {
2351             oldPiece.setWidth(oldValue);
2352           } else if ("DEPTH" == propertyName) {
2353             oldPiece.setDepth(oldValue);
2354           }
2355           // For doors and windows, propertyName can't be equal to ROLL or PITCH
2356 
2357           component3D.updateIntersectingWalls([oldPiece, piece]);
2358         } else {
2359           component3D.updateIntersectingWalls([piece]);
2360         }
2361         
2362         component3D.updateObjects(component3D.home.getWalls());
2363       } else if (component3D.containsStaircases(piece)) {
2364         component3D.updateObjects(component3D.home.getRooms());
2365       }
2366       if (piece.getLevel() !== null && piece.getLevel().getElevation() < 0) {
2367         component3D.groundChangeListener(null);
2368       }
2369     };  
2370   this.furnitureChangeListener = function(ev) {
2371       var updatedPiece = ev.getSource();
2372       var propertyName = ev.getPropertyName();
2373       if ("X" == propertyName
2374           || "Y" == propertyName
2375           || "ANGLE" == propertyName
2376           || "ROLL" == propertyName
2377           || "PITCH" == propertyName
2378           || "WIDTH" == propertyName
2379           || "DEPTH" == propertyName) {
2380         updatePieceOfFurnitureGeometry(updatedPiece, propertyName, ev.getOldValue());
2381       } else if ("HEIGHT" == propertyName
2382           || "ELEVATION" == propertyName
2383           || "MODEL" == propertyName
2384           || "MODEL_ROTATION" == propertyName
2385           || "MODEL_MIRRORED" == propertyName
2386           || "MODEL_FLAGS" == propertyName
2387           || "MODEL_TRANSFORMATIONS" == propertyName
2388           || "STAIRCASE_CUT_OUT_SHAPE" == propertyName
2389           || "VISIBLE" == propertyName
2390           || "LEVEL" == propertyName) {
2391         updatePieceOfFurnitureGeometry(updatedPiece, null, null);
2392       } else if ("CUT_OUT_SHAPE" == propertyName
2393           || "WALL_CUT_OUT_ON_BOTH_SIDES" == propertyName
2394           || "WALL_WIDTH" == propertyName
2395           || "WALL_LEFT" == propertyName
2396           || "WALL_HEIGHT" == propertyName
2397           || "WALL_TOP" == propertyName) {
2398         if (component3D.containsDoorsAndWindows(updatedPiece)) {
2399           component3D.updateIntersectingWalls([updatedPiece]);
2400         }
2401       } else if ("COLOR" == propertyName
2402           || "TEXTURE" == propertyName
2403           || "MODEL_MATERIALS" == propertyName
2404           || "SHININESS" == propertyName
2405           || ("POWER" == propertyName
2406               && component3D.home.getEnvironment().getSubpartSizeUnderLight() > 0)) {
2407         component3D.updateObjects([updatedPiece]);
2408       }
2409     };
2410 
2411   var furniture = this.home.getFurniture();
2412   for (var i = 0; i < furniture.length; i++) { 
2413     this.addPropertyChangeListener(furniture [i], this.furnitureChangeListener);
2414   }      
2415   this.furnitureListener = function(ev) {
2416       var piece = ev.getItem();
2417       switch (ev.getType()) {
2418         case CollectionEvent.Type.ADD :
2419           component3D.addPieceOfFurniture(group, piece, true, false);
2420           component3D.addPropertyChangeListener(piece, component3D.furnitureChangeListener);
2421           break;
2422         case CollectionEvent.Type.DELETE : 
2423           component3D.deletePieceOfFurniture(piece);
2424           component3D.removePropertyChangeListener(piece, component3D.furnitureChangeListener);
2425           break;
2426       }
2427       // If piece is or contains a door or a window, update walls that intersect with piece
2428       if (component3D.containsDoorsAndWindows(piece)) {
2429         component3D.updateIntersectingWalls([piece]);
2430       } else if (component3D.containsStaircases(piece)) {
2431         component3D.updateObjects(component3D.home.getRooms());
2432       } else {
2433         component3D.approximateHomeBoundsCache = null;
2434         component3D.homeHeightCache = null;
2435       }
2436       component3D.groundChangeListener(null);
2437     };
2438   this.home.addFurnitureListener(this.furnitureListener);
2439 }
2440 
2441 /**
2442  * Adds the given <code>listener</code> to <code>piece</code> and its children.
2443  * @param {HomePieceOfFurniture} piece
2444  * @param {PropertyChangeListener} listener
2445  * @private
2446  */
2447 HomeComponent3D.prototype.addPropertyChangeListener = function(piece, listener) {
2448   if (piece instanceof HomeFurnitureGroup) {
2449     var furniture = piece.getFurniture();
2450     for (var i = 0; i < furniture.length; i++) {
2451       this.addPropertyChangeListener(furniture [i], listener);
2452     }
2453   } else {
2454     piece.addPropertyChangeListener(listener);
2455   }
2456 }
2457 
2458 /**
2459  * Removes the given <code>listener</code> from <code>piece</code> and its children.
2460  * @param {HomePieceOfFurniture} piece
2461  * @param {PropertyChangeListener} listener
2462  * @private
2463  */
2464 HomeComponent3D.prototype.removePropertyChangeListener = function(piece, listener) {
2465   if (piece instanceof HomeFurnitureGroup) {
2466     var furniture = piece.getFurniture();
2467     for (var i = 0; i < furniture.length; i++) {
2468       this.removePropertyChangeListener(furniture [i], listener);
2469     }
2470   } else {
2471     piece.removePropertyChangeListener(listener);
2472   }
2473 }
2474 
2475 /**
2476  * Returns <code>true</code> if the given <code>piece</code> is or contains a door or window.
2477  * @param {HomePieceOfFurniture} piece
2478  * @return {boolean}
2479  * @private
2480  */
2481 HomeComponent3D.prototype.containsDoorsAndWindows = function(piece) {
2482   if (piece instanceof HomeFurnitureGroup) {
2483     var furniture = piece.getFurniture();
2484     for (var i = 0; i < furniture.length; i++) {
2485       if (this.containsDoorsAndWindows(furniture[i])) {
2486         return true;
2487       }
2488     }
2489     return false;
2490   } else {
2491     return piece.isDoorOrWindow();
2492   }
2493 }
2494 
2495 /**
2496  * Returns <code>true</code> if the given <code>piece</code> is or contains a staircase
2497  * with a top cut out shape.
2498  * @param {HomePieceOfFurniture} piece
2499  * @return {boolean}
2500  * @private
2501  */
2502 HomeComponent3D.prototype.containsStaircases = function(piece) {
2503   if (piece instanceof HomeFurnitureGroup) {
2504     var furniture = piece.getFurniture();
2505     for (var i = 0; i < furniture.length; i++) {
2506       if (this.containsStaircases(furniture[i])) {
2507         return true;
2508       }
2509     }
2510     return false;
2511   } else {
2512     return piece.getStaircaseCutOutShape() !== null;
2513   }
2514 }
2515 
2516 /**
2517  * Adds a room listener to home rooms that updates the children of the given
2518  * <code>group</code>, each time a room is added, updated or deleted.
2519  * @param {Group3D} group
2520  * @private
2521  */
2522 HomeComponent3D.prototype.addRoomListener = function(group) {
2523   var component3D = this;
2524   this.roomChangeListener = function(ev) {
2525       var updatedRoom = ev.getSource();
2526       var propertyName = ev.getPropertyName();
2527       if ("FLOOR_COLOR" == propertyName
2528           || "FLOOR_TEXTURE" == propertyName
2529           || "FLOOR_SHININESS" == propertyName
2530           || "CEILING_COLOR" == propertyName
2531           || "CEILING_TEXTURE" == propertyName
2532           || "CEILING_SHININESS" == propertyName
2533           || "CEILING_FLAT" == propertyName) {
2534         component3D.updateObjects([updatedRoom]);
2535       } else if ("FLOOR_VISIBLE" == propertyName
2536           || "CEILING_VISIBLE" == propertyName
2537           || "LEVEL" == propertyName) {   
2538         component3D.updateObjects(component3D.home.getRooms());
2539         component3D.groundChangeListener(null);
2540       } else if ("POINTS" == propertyName) {   
2541         if (component3D.homeObjectsToUpdate) {
2542           // Don't try to optimize if more than one room to update
2543           component3D.updateObjects(component3D.home.getRooms());
2544         } else {
2545           component3D.updateObjects([updatedRoom]);
2546           // Search the rooms that overlap the updated one
2547           var oldArea = new java.awt.geom.Area(component3D.getShape(ev.getOldValue()));
2548           var newArea = new java.awt.geom.Area(component3D.getShape(ev.getNewValue()));
2549           var updatedRoomLevel = updatedRoom.getLevel(); 
2550           var rooms = component3D.home.getRooms();
2551           for (var i = 0; i < rooms.length; i++) {
2552             var room = rooms[i];
2553             var roomLevel = room.getLevel();
2554             if (room != updatedRoom
2555                 && (roomLevel == null
2556                     || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() + roomLevel.getHeight())) < 1E-5
2557                     || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() - roomLevel.getFloorThickness())) < 1E-5)) {
2558               var roomAreaIntersectionWithOldArea = new java.awt.geom.Area(component3D.getShape(room.getPoints()));
2559               var roomAreaIntersectionWithNewArea = new java.awt.geom.Area(roomAreaIntersectionWithOldArea);
2560               roomAreaIntersectionWithNewArea.intersect(newArea);                  
2561               if (!roomAreaIntersectionWithNewArea.isEmpty()) {
2562                 component3D.updateObjects([room]);
2563               } else {
2564                 roomAreaIntersectionWithOldArea.intersect(oldArea);
2565                 if (!roomAreaIntersectionWithOldArea.isEmpty()) {
2566                   component3D.updateObjects([room]);
2567                 }
2568               }
2569             }
2570           }              
2571         }
2572         component3D.groundChangeListener(null);
2573       }            
2574     };
2575   var rooms = this.home.getRooms();
2576   for (var i = 0; i < rooms.length; i++) {
2577     rooms[i].addPropertyChangeListener(this.roomChangeListener);
2578   }
2579   this.roomListener = function(ev) {
2580       var room = ev.getItem();
2581       switch (ev.getType()) {
2582         case CollectionEvent.Type.ADD :
2583           component3D.addObject(group, room, ev.getIndex(), true, false);
2584           room.addPropertyChangeListener(component3D.roomChangeListener);
2585           break;
2586         case CollectionEvent.Type.DELETE :
2587           component3D.deleteObject(room);
2588           room.removePropertyChangeListener(component3D.roomChangeListener);
2589           break;
2590       }
2591       component3D.updateObjects(component3D.home.getRooms());
2592       component3D.groundChangeListener(null);
2593     };
2594   this.home.addRoomsListener(this.roomListener);
2595 }
2596 
2597 /**
2598  * Returns the path matching points.
2599  * @param {Array} points
2600  * @return {GeneralPath}
2601  * @private
2602  */
2603 HomeComponent3D.prototype.getShape = function(points) {
2604   var path = new java.awt.geom.GeneralPath();
2605   path.moveTo(points[0][0], points[0][1]);
2606   for (var i = 1; i < points.length; i++) {
2607     path.lineTo(points[i][0], points[i][1]);
2608   }
2609   path.closePath();
2610   return path;
2611 }
2612 
2613 /**
2614  * Adds a polyline listener to home polylines that updates the children of the given
2615  * <code>group</code>, each time a polyline is added, updated or deleted.
2616  * @param {Group} group
2617  * @private
2618  */
2619 HomeComponent3D.prototype.addPolylineListener = function(group) {
2620   var component3D = this;
2621   this.polylineChangeListener = function(ev) {
2622       var polyline = ev.getSource();
2623       component3D.updateObjects([polyline]);
2624     };
2625   var polylines = this.home.getPolylines();
2626   for (var i = 0; i < polylines.length; i++) {
2627     polylines[i].addPropertyChangeListener(this.polylineChangeListener);
2628   }
2629   this.polylineListener = function(ev) {
2630       var polyline = ev.getItem();
2631       switch (ev.getType()) {
2632         case CollectionEvent.Type.ADD :
2633           component3D.addObject(group, polyline, true, false);
2634           polyline.addPropertyChangeListener(component3D.polylineChangeListener);
2635           break;
2636         case CollectionEvent.Type.DELETE :
2637           component3D.deleteObject(polyline);
2638           polyline.removePropertyChangeListener(component3D.polylineChangeListener);
2639           break;
2640       }
2641     };
2642   this.home.addPolylinesListener(this.polylineListener);
2643 }
2644 
2645 /**
2646  * Adds a dimension line listener to home dimension lines that updates the children of the given
2647  * <code>group</code>, each time a dimension line is added, updated or deleted.
2648  * @param {Group3D} group
2649  * @private
2650  */
2651 HomeComponent3D.prototype.addDimensionLineListener = function(group) {
2652   var component3D = this;
2653   this.dimensionLineChangeListener = function(ev) {
2654       var updatedDimensionLine = ev.getSource();
2655       component3D.updateObjects([updatedDimensionLine]);
2656     };
2657   var dimensionLines = this.home.getDimensionLines();
2658   for (var i = 0; i < dimensionLines.length; i++) {
2659     dimensionLines [i].addPropertyChangeListener(this.dimensionLineChangeListener);
2660   }
2661   this.dimensionLineListener = function(ev) {
2662       var dimensionLine = ev.getItem();
2663       switch (ev.getType()) {
2664         case CollectionEvent.Type.ADD :
2665           component3D.addObject(group, dimensionLine, true, false);
2666           dimensionLine.addPropertyChangeListener(component3D.dimensionLineChangeListener);
2667           break;
2668         case CollectionEvent.Type.DELETE :
2669           component3D.deleteObject(dimensionLine);
2670           dimensionLine.removePropertyChangeListener(component3D.dimensionLineChangeListener);
2671           break;
2672       }
2673     };
2674   this.home.addDimensionLinesListener(this.dimensionLineListener);
2675 }
2676 
2677  /**
2678  * Adds a label listener to home labels that updates the children of the given
2679  * <code>group</code>, each time a label is added, updated or deleted.
2680  * @param {Group3D} group
2681  * @private
2682  */
2683 HomeComponent3D.prototype.addLabelListener = function(group) {
2684   var component3D = this;
2685   this.labelChangeListener = function(ev) {
2686       var label = ev.getSource();
2687       component3D.updateObjects([label]);
2688     };
2689   var labels = this.home.getLabels();
2690   for (var i = 0; i < labels.length; i++) {
2691     labels[i].addPropertyChangeListener(this.labelChangeListener);
2692   }
2693   this.labelListener = function(ev) {
2694       var label = ev.getItem();
2695       switch (ev.getType()) {
2696         case CollectionEvent.Type.ADD :
2697           component3D.addObject(group, label, true, false);
2698           label.addPropertyChangeListener(component3D.labelChangeListener);
2699           break;
2700         case CollectionEvent.Type.DELETE :
2701           component3D.deleteObject(label);
2702           label.removePropertyChangeListener(component3D.labelChangeListener);
2703           break;
2704       }
2705     };
2706   this.home.addLabelsListener(this.labelListener);
2707 }
2708 
2709 /**
2710  * Adds a walls alpha change listener and drawing mode change listener to home
2711  * environment that updates the home scene objects appearance.
2712  * @private
2713  */
2714 HomeComponent3D.prototype.addEnvironmentListeners = function() {
2715   var component3D = this;
2716   this.wallsAlphaListener = function(ev) {
2717       component3D.updateObjects(component3D.home.getWalls());
2718       component3D.updateObjects(component3D.home.getRooms());
2719     };
2720   this.home.getEnvironment().addPropertyChangeListener("WALLS_ALPHA", this.wallsAlphaListener);
2721 }
2722 
2723 /**
2724  * Adds to <code>group</code> a branch matching <code>homeObject</code> at a given <code>index</code>.
2725  * If <code>index</code> is missing or equal to -1, <code>homeObject</code> will be added at the end of the group.
2726  * @param {Group3D} group
2727  * @param {Object}  homeObject
2728  * @param {number}  [index]
2729  * @param {boolean} listenToHomeUpdates
2730  * @param {boolean} waitForLoading
2731  * @private
2732  */
2733 HomeComponent3D.prototype.addObject = function(group, homeObject, index, 
2734                                                listenToHomeUpdates, waitForLoading) {
2735   if (waitForLoading === undefined) {
2736     waitForLoading = listenToHomeUpdates;
2737     listenToHomeUpdates = index;
2738     index = -1;
2739   }
2740   var object3D = this.object3dFactory.createObject3D(this.home, homeObject, waitForLoading);
2741   if (listenToHomeUpdates) {
2742     homeObject.object3D = object3D;
2743     this.homeObjects.push(homeObject);
2744     this.homeObjects3D.push(object3D);
2745   }
2746   if (index === -1) {
2747     group.addChild(object3D);
2748   } else {
2749     group.insertChild(object3D, index);
2750   }
2751   return object3D;
2752 }
2753 
2754 /**
2755  * Adds to <code>group</code> a branch matching <code>homeObject</code> or its children if the piece is a group of furniture.
2756  * @param {Group3D} group
2757  * @param {HomePieceOfFurniture} piece
2758  * @param {boolean} listenToHomeUpdates
2759  * @param {boolean} waitForLoading
2760  * @private
2761  */
2762 HomeComponent3D.prototype.addPieceOfFurniture = function(group, piece, listenToHomeUpdates, waitForLoading) {
2763   if (piece instanceof HomeFurnitureGroup) {
2764     var furniture = piece.getFurniture();
2765     for (var i = 0; i < furniture.length; i++) {
2766       this.addPieceOfFurniture(group, furniture [i], listenToHomeUpdates, waitForLoading);
2767     }
2768   } else {
2769     this.addObject(group, piece, listenToHomeUpdates, waitForLoading);
2770   }
2771 }
2772 
2773 /**
2774  * Detaches from the scene the branch matching <code>homeObject</code>.
2775  * @param {Object}  homeObject
2776  * @private
2777  */
2778 HomeComponent3D.prototype.deleteObject = function(homeObject) {
2779   if (homeObject.object3D) {
2780     homeObject.object3D.detach();
2781     delete homeObject.object3D;
2782     var objectIndex = this.homeObjects.indexOf(homeObject);
2783     this.homeObjects.splice(objectIndex, 1);
2784     this.homeObjects3D.splice(objectIndex, 1);
2785     if (this.homeObjectsToUpdate) {
2786       var index = this.homeObjectsToUpdate.indexOf(homeObject);
2787       if (index >= 0) {
2788         this.homeObjectsToUpdate.splice(index, 1);
2789       }
2790     }
2791   }
2792 }
2793 
2794 /**
2795  * Detaches from the scene the branches matching <code>piece</code> or its children if it's a group.
2796  * @param {HomePieceOfFurniture} piece
2797  * @private
2798  */
2799 HomeComponent3D.prototype.deletePieceOfFurniture = function(piece) {
2800   if (piece instanceof HomeFurnitureGroup) {
2801     var furniture = piece.getFurniture();
2802     for (var i = 0; i < furniture.length; i++) {
2803       this.deletePieceOfFurniture(furniture [i]);
2804     }
2805   } else {
2806     this.deleteObject(piece);
2807   }
2808 }
2809 
2810 /**
2811  * Updates 3D <code>objects</code> later. 
2812  * @param {Array} objects
2813  * @private
2814  */
2815 HomeComponent3D.prototype.updateObjects = function(objects) {
2816   if (this.homeObjectsToUpdate) {
2817     for (var i = 0; i < objects.length; i++) {
2818       var object = objects [i];
2819       if (this.homeObjectsToUpdate.indexOf(object) <= -1) {        
2820         this.homeObjectsToUpdate.push(object);
2821       }
2822     }
2823   } else {
2824     this.homeObjectsToUpdate = objects.slice(0);
2825     // Invoke later the update of objects of homeObjectsToUpdate
2826     setTimeout(
2827         function(component3D) {
2828           for (var i = 0; i < component3D.homeObjectsToUpdate.length; i++) {
2829             var homeObject = component3D.homeObjectsToUpdate [i];
2830             // Check object wasn't deleted since updateObjects call
2831             if (homeObject.object3D) { 
2832               homeObject.object3D.update();
2833             }
2834           }
2835           delete component3D.homeObjectsToUpdate;
2836         }, 0, this);
2837   }
2838   this.approximateHomeBoundsCache = null;
2839   this.homeHeightCache = null;
2840 }
2841 
2842 /**
2843  * Updates 3D objects and furniture groups children, if <code>objects</code> contains some groups.
2844  * @param {Array} objects
2845  * @private
2846  */
2847 HomeComponent3D.prototype.updateObjectsAndFurnitureGroups = function(objects) {
2848   this.updateObjects(objects);
2849   for (var i = 0; i < objects.length; i++) {
2850     var item = objects [i];
2851     if (item instanceof HomeFurnitureGroup) {
2852       this.updateObjects(item.getAllFurniture());
2853     }
2854   }
2855 }
2856 
2857 /**
2858  * Updates walls that may intersect from the given doors or window.
2859  * @param {Array} doorOrWindows
2860  * @private
2861  */
2862 HomeComponent3D.prototype.updateIntersectingWalls = function(doorOrWindows) {
2863   var walls = this.home.getWalls();
2864   var wallCount = 0;
2865   if (this.homeObjectsToUpdate) {
2866     for (var i = 0; i < this.homeObjectsToUpdate.length; i++) {
2867       if (this.homeObjectsToUpdate [i] instanceof Wall) {
2868         wallCount++;
2869       }
2870     }
2871   }
2872 
2873   if (wallCount !== walls.length) {
2874     var updatedWalls = [];
2875     var doorOrWindowBounds = null;
2876     for (var i = 0; i < doorOrWindows.length; i++) {
2877       var doorOrWindow = doorOrWindows [i];
2878       var points = doorOrWindow.getPoints();
2879       if (doorOrWindowBounds === null) {
2880         doorOrWindowBounds = new java.awt.geom.Rectangle2D.Float(points [0][0], points [0][1], 0, 0);
2881       } else {
2882         doorOrWindowBounds.add(points [0][0], points [0][1]);
2883       }
2884       for (var j = 1; j < points.length; j++) {
2885         doorOrWindowBounds.add(points [j][0], points [j][1]);
2886       }
2887     }
2888     // Search walls that intersect approximative bounds
2889     for (var i = 0; i < walls.length; i++) {
2890       var wall = walls [i];
2891       if (wall.intersectsRectangle(doorOrWindowBounds.getX(), doorOrWindowBounds.getY(),
2892           doorOrWindowBounds.getX() + doorOrWindowBounds.getWidth(),
2893           doorOrWindowBounds.getY() + doorOrWindowBounds.getHeight())) {
2894         updatedWalls.push(wall);
2895       }
2896     }
2897     this.updateObjects(updatedWalls);
2898   }
2899 }
2900 
2901 /**
2902  * Updates <code>wall</code> geometry,
2903  * and the walls at its end or start.
2904  * @param {Wall} wall
2905  * @private
2906  */
2907 HomeComponent3D.prototype.updateWall = function(wall) {
2908   var wallsToUpdate = [];
2909   wallsToUpdate.push(wall);
2910   if (wall.getWallAtStart() != null) {
2911     wallsToUpdate.push(wall.getWallAtStart());
2912   }
2913   if (wall.getWallAtEnd() != null) {
2914     wallsToUpdate.push(wall.getWallAtEnd());
2915   }
2916   this.updateObjects(wallsToUpdate);
2917 }
2918 
2919 
2920 /**
2921  * A factory able to create instances of {@link Object3DBranch} class.
2922  * @param {UserPreferences} [preferences]
2923  * @constructor
2924  * @author Emmanuel Puybaret
2925  */
2926 function Object3DBranchFactory(preferences) {
2927   if (preferences !== undefined) {
2928     this.preferences = preferences;
2929   }
2930 }
2931 
2932 /**
2933  * Returns the 3D object matching a given <code>item</code>.
2934  * @param {Home} home
2935  * @param {Object} item
2936  * @param {boolean|function} waitForLoading 
2937  * @return {Object3DBranch} an instance of a subclass of {@link Object3DBranch}
2938  */
2939 Object3DBranchFactory.prototype.createObject3D = function(home, item, waitForLoading) {
2940   if (item instanceof HomePieceOfFurniture) {
2941     return new HomePieceOfFurniture3D(item, home, this.preferences, waitForLoading);
2942   } else if (item instanceof Wall) {
2943     return new Wall3D(item, home, this.preferences, waitForLoading);
2944   } else if (item instanceof Room) {
2945     return new Room3D(item, home, this.preferences, false, waitForLoading);
2946   } else if (item instanceof Polyline) {
2947     return new Polyline3D(item, home, this.preferences, waitForLoading);
2948   } else if (item instanceof DimensionLine) {
2949     return new DimensionLine3D(item, home, this.preferences, waitForLoading);
2950   } else if (item instanceof Label) {
2951      return new Label3D(item, home, this.preferences, waitForLoading);
2952   } else {
2953     return new Group3D();
2954   }  
2955 }
2956