1 /*
  2  * HomeComponent3D.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.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                  || "ELEVATION_INDEX" == propertyName) {
2257         component3D.updateObjects(component3D.homeObjects.slice(0));          
2258         component3D.groundChangeListener(null);
2259       } else if ("BACKGROUND_IMAGE" == propertyName) {
2260         component3D.groundChangeListener(null);
2261       } else if ("FLOOR_THICKNESS" == propertyName) {
2262         component3D.updateObjects(component3D.home.getWalls());          
2263         component3D.updateObjects(component3D.home.getRooms());
2264       } else if ("HEIGHT" == propertyName) {
2265         component3D.updateObjects(component3D.home.getRooms());
2266       }  
2267     };
2268   var levels = this.home.getLevels();
2269   for (var i = 0; i < levels.length; i++) {
2270     levels[i].addPropertyChangeListener(this.levelChangeListener);
2271   }
2272 
2273   this.levelListener = function(ev) {
2274       var level = ev.getItem();
2275       switch ((ev.getType())) {
2276         case CollectionEvent.Type.ADD :
2277           level.addPropertyChangeListener(component3D.levelChangeListener);
2278           break;
2279         case CollectionEvent.Type.DELETE :
2280           level.removePropertyChangeListener(component3D.levelChangeListener);
2281           break;
2282         }
2283       component3D.updateObjects(component3D.home.getRooms());
2284     };
2285   this.home.addLevelsListener(this.levelListener);
2286 }
2287 
2288 /**
2289  * Adds a wall listener to home walls that updates the children of the given
2290  * <code>group</code>, each time a wall is added, updated or deleted.
2291  * @param {Group3D} group
2292  * @private
2293  */
2294 HomeComponent3D.prototype.addWallListener = function(group) {
2295   var component3D = this;
2296   this.wallChangeListener = function(ev) {
2297       var propertyName = ev.getPropertyName();
2298       if ("PATTERN" != propertyName) {
2299         var updatedWall = ev.getSource();
2300         component3D.updateWall(updatedWall);          
2301         var levels = component3D.home.getLevels();
2302         if (updatedWall.getLevel() === null
2303             || updatedWall.isAtLevel(levels [levels.length - 1])) {
2304           component3D.updateObjects(component3D.home.getRooms());
2305         }
2306         if (updatedWall.getLevel() != null && updatedWall.getLevel().getElevation() < 0) {
2307           component3D.groundChangeListener(null);
2308         }
2309       }
2310     };
2311   var walls = this.home.getWalls();
2312   for (var i = 0; i < walls.length; i++) {
2313     walls[i].addPropertyChangeListener(this.wallChangeListener);
2314   }
2315   this.wallListener = function(ev) {
2316       var wall = ev.getItem();
2317       switch ((ev.getType())) {
2318         case CollectionEvent.Type.ADD :
2319           component3D.addObject(group, wall, true, false);
2320           wall.addPropertyChangeListener(component3D.wallChangeListener);
2321           break;
2322         case CollectionEvent.Type.DELETE :
2323           component3D.deleteObject(wall);
2324           wall.removePropertyChangeListener(component3D.wallChangeListener);
2325           break;
2326       }
2327       component3D.updateObjects(component3D.home.getRooms());
2328       component3D.groundChangeListener(null);
2329     };
2330   this.home.addWallsListener(this.wallListener);
2331 }
2332 
2333 /**
2334  * Adds a furniture listener to home that updates the children of the given <code>group</code>, 
2335  * each time a piece of furniture is added, updated or deleted.
2336  * @private 
2337  */
2338 HomeComponent3D.prototype.addFurnitureListener = function(group) {
2339   var component3D = this;
2340   var updatePieceOfFurnitureGeometry = function(piece, propertyName, oldValue) {
2341       component3D.updateObjects([piece]);
2342       if (component3D.containsDoorsAndWindows(piece)) {
2343         if (oldValue !== null) {
2344           var oldPiece = piece.clone();
2345           if ("X" == propertyName) {
2346             oldPiece.setX(oldValue);
2347           } else if ("Y" == propertyName) {
2348             oldPiece.setY(oldValue);
2349           } else if ("ANGLE" == propertyName) {
2350             oldPiece.setAngle(oldValue);
2351           } else if ("WIDTH" == propertyName) {
2352             oldPiece.setWidth(oldValue);
2353           } else if ("DEPTH" == propertyName) {
2354             oldPiece.setDepth(oldValue);
2355           }
2356           // For doors and windows, propertyName can't be equal to ROLL or PITCH
2357 
2358           component3D.updateIntersectingWalls([oldPiece, piece]);
2359         } else {
2360           component3D.updateIntersectingWalls([piece]);
2361         }
2362         
2363         component3D.updateObjects(component3D.home.getWalls());
2364       } else if (component3D.containsStaircases(piece)) {
2365         component3D.updateObjects(component3D.home.getRooms());
2366       }
2367       if (piece.getLevel() !== null && piece.getLevel().getElevation() < 0) {
2368         component3D.groundChangeListener(null);
2369       }
2370     };  
2371   this.furnitureChangeListener = function(ev) {
2372       var updatedPiece = ev.getSource();
2373       var propertyName = ev.getPropertyName();
2374       if ("X" == propertyName
2375           || "Y" == propertyName
2376           || "ANGLE" == propertyName
2377           || "ROLL" == propertyName
2378           || "PITCH" == propertyName
2379           || "WIDTH" == propertyName
2380           || "DEPTH" == propertyName) {
2381         updatePieceOfFurnitureGeometry(updatedPiece, propertyName, ev.getOldValue());
2382       } else if ("HEIGHT" == propertyName
2383           || "ELEVATION" == propertyName
2384           || "MODEL" == propertyName
2385           || "MODEL_ROTATION" == propertyName
2386           || "MODEL_MIRRORED" == propertyName
2387           || "MODEL_FLAGS" == propertyName
2388           || "MODEL_TRANSFORMATIONS" == propertyName
2389           || "STAIRCASE_CUT_OUT_SHAPE" == propertyName
2390           || "VISIBLE" == propertyName
2391           || "LEVEL" == propertyName) {
2392         updatePieceOfFurnitureGeometry(updatedPiece, null, null);
2393       } else if ("CUT_OUT_SHAPE" == propertyName
2394           || "WALL_CUT_OUT_ON_BOTH_SIDES" == propertyName
2395           || "WALL_WIDTH" == propertyName
2396           || "WALL_LEFT" == propertyName
2397           || "WALL_HEIGHT" == propertyName
2398           || "WALL_TOP" == propertyName) {
2399         if (component3D.containsDoorsAndWindows(updatedPiece)) {
2400           component3D.updateIntersectingWalls([updatedPiece]);
2401         }
2402       } else if ("COLOR" == propertyName
2403           || "TEXTURE" == propertyName
2404           || "MODEL_MATERIALS" == propertyName
2405           || "SHININESS" == propertyName
2406           || ("POWER" == propertyName
2407               && component3D.home.getEnvironment().getSubpartSizeUnderLight() > 0)) {
2408         component3D.updateObjects([updatedPiece]);
2409       }
2410     };
2411 
2412   var furniture = this.home.getFurniture();
2413   for (var i = 0; i < furniture.length; i++) { 
2414     this.addPropertyChangeListener(furniture [i], this.furnitureChangeListener);
2415   }      
2416   this.furnitureListener = function(ev) {
2417       var piece = ev.getItem();
2418       switch (ev.getType()) {
2419         case CollectionEvent.Type.ADD :
2420           component3D.addPieceOfFurniture(group, piece, true, false);
2421           component3D.addPropertyChangeListener(piece, component3D.furnitureChangeListener);
2422           break;
2423         case CollectionEvent.Type.DELETE : 
2424           component3D.deletePieceOfFurniture(piece);
2425           component3D.removePropertyChangeListener(piece, component3D.furnitureChangeListener);
2426           break;
2427       }
2428       // If piece is or contains a door or a window, update walls that intersect with piece
2429       if (component3D.containsDoorsAndWindows(piece)) {
2430         component3D.updateIntersectingWalls([piece]);
2431       } else if (component3D.containsStaircases(piece)) {
2432         component3D.updateObjects(component3D.home.getRooms());
2433       } else {
2434         component3D.approximateHomeBoundsCache = null;
2435         component3D.homeHeightCache = null;
2436       }
2437       component3D.groundChangeListener(null);
2438     };
2439   this.home.addFurnitureListener(this.furnitureListener);
2440 }
2441 
2442 /**
2443  * Adds the given <code>listener</code> to <code>piece</code> and its children.
2444  * @param {HomePieceOfFurniture} piece
2445  * @param {PropertyChangeListener} listener
2446  * @private
2447  */
2448 HomeComponent3D.prototype.addPropertyChangeListener = function(piece, listener) {
2449   if (piece instanceof HomeFurnitureGroup) {
2450     var furniture = piece.getFurniture();
2451     for (var i = 0; i < furniture.length; i++) {
2452       this.addPropertyChangeListener(furniture [i], listener);
2453     }
2454   } else {
2455     piece.addPropertyChangeListener(listener);
2456   }
2457 }
2458 
2459 /**
2460  * Removes the given <code>listener</code> from <code>piece</code> and its children.
2461  * @param {HomePieceOfFurniture} piece
2462  * @param {PropertyChangeListener} listener
2463  * @private
2464  */
2465 HomeComponent3D.prototype.removePropertyChangeListener = function(piece, listener) {
2466   if (piece instanceof HomeFurnitureGroup) {
2467     var furniture = piece.getFurniture();
2468     for (var i = 0; i < furniture.length; i++) {
2469       this.removePropertyChangeListener(furniture [i], listener);
2470     }
2471   } else {
2472     piece.removePropertyChangeListener(listener);
2473   }
2474 }
2475 
2476 /**
2477  * Returns <code>true</code> if the given <code>piece</code> is or contains a door or window.
2478  * @param {HomePieceOfFurniture} piece
2479  * @return {boolean}
2480  * @private
2481  */
2482 HomeComponent3D.prototype.containsDoorsAndWindows = function(piece) {
2483   if (piece instanceof HomeFurnitureGroup) {
2484     var furniture = piece.getFurniture();
2485     for (var i = 0; i < furniture.length; i++) {
2486       if (this.containsDoorsAndWindows(furniture[i])) {
2487         return true;
2488       }
2489     }
2490     return false;
2491   } else {
2492     return piece.isDoorOrWindow();
2493   }
2494 }
2495 
2496 /**
2497  * Returns <code>true</code> if the given <code>piece</code> is or contains a staircase
2498  * with a top cut out shape.
2499  * @param {HomePieceOfFurniture} piece
2500  * @return {boolean}
2501  * @private
2502  */
2503 HomeComponent3D.prototype.containsStaircases = function(piece) {
2504   if (piece instanceof HomeFurnitureGroup) {
2505     var furniture = piece.getFurniture();
2506     for (var i = 0; i < furniture.length; i++) {
2507       if (this.containsStaircases(furniture[i])) {
2508         return true;
2509       }
2510     }
2511     return false;
2512   } else {
2513     return piece.getStaircaseCutOutShape() !== null;
2514   }
2515 }
2516 
2517 /**
2518  * Adds a room listener to home rooms that updates the children of the given
2519  * <code>group</code>, each time a room is added, updated or deleted.
2520  * @param {Group3D} group
2521  * @private
2522  */
2523 HomeComponent3D.prototype.addRoomListener = function(group) {
2524   var component3D = this;
2525   this.roomChangeListener = function(ev) {
2526       var updatedRoom = ev.getSource();
2527       var propertyName = ev.getPropertyName();
2528       if ("FLOOR_COLOR" == propertyName
2529           || "FLOOR_TEXTURE" == propertyName
2530           || "FLOOR_SHININESS" == propertyName
2531           || "CEILING_COLOR" == propertyName
2532           || "CEILING_TEXTURE" == propertyName
2533           || "CEILING_SHININESS" == propertyName
2534           || "CEILING_FLAT" == propertyName) {
2535         component3D.updateObjects([updatedRoom]);
2536       } else if ("FLOOR_VISIBLE" == propertyName
2537           || "CEILING_VISIBLE" == propertyName
2538           || "LEVEL" == propertyName) {   
2539         component3D.updateObjects(component3D.home.getRooms());
2540         component3D.groundChangeListener(null);
2541       } else if ("POINTS" == propertyName) {   
2542         if (component3D.homeObjectsToUpdate) {
2543           // Don't try to optimize if more than one room to update
2544           component3D.updateObjects(component3D.home.getRooms());
2545         } else {
2546           component3D.updateObjects([updatedRoom]);
2547           // Search the rooms that overlap the updated one
2548           var oldArea = new java.awt.geom.Area(component3D.getShape(ev.getOldValue()));
2549           var newArea = new java.awt.geom.Area(component3D.getShape(ev.getNewValue()));
2550           var updatedRoomLevel = updatedRoom.getLevel(); 
2551           var rooms = component3D.home.getRooms();
2552           for (var i = 0; i < rooms.length; i++) {
2553             var room = rooms[i];
2554             var roomLevel = room.getLevel();
2555             if (room != updatedRoom
2556                 && (roomLevel == null
2557                     || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() + roomLevel.getHeight())) < 1E-5
2558                     || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() - roomLevel.getFloorThickness())) < 1E-5)) {
2559               var roomAreaIntersectionWithOldArea = new java.awt.geom.Area(component3D.getShape(room.getPoints()));
2560               var roomAreaIntersectionWithNewArea = new java.awt.geom.Area(roomAreaIntersectionWithOldArea);
2561               roomAreaIntersectionWithNewArea.intersect(newArea);                  
2562               if (!roomAreaIntersectionWithNewArea.isEmpty()) {
2563                 component3D.updateObjects([room]);
2564               } else {
2565                 roomAreaIntersectionWithOldArea.intersect(oldArea);
2566                 if (!roomAreaIntersectionWithOldArea.isEmpty()) {
2567                   component3D.updateObjects([room]);
2568                 }
2569               }
2570             }
2571           }              
2572         }
2573         component3D.groundChangeListener(null);
2574       }            
2575     };
2576   var rooms = this.home.getRooms();
2577   for (var i = 0; i < rooms.length; i++) {
2578     rooms[i].addPropertyChangeListener(this.roomChangeListener);
2579   }
2580   this.roomListener = function(ev) {
2581       var room = ev.getItem();
2582       switch (ev.getType()) {
2583         case CollectionEvent.Type.ADD :
2584           component3D.addObject(group, room, ev.getIndex(), true, false);
2585           room.addPropertyChangeListener(component3D.roomChangeListener);
2586           break;
2587         case CollectionEvent.Type.DELETE :
2588           component3D.deleteObject(room);
2589           room.removePropertyChangeListener(component3D.roomChangeListener);
2590           break;
2591       }
2592       component3D.updateObjects(component3D.home.getRooms());
2593       component3D.groundChangeListener(null);
2594     };
2595   this.home.addRoomsListener(this.roomListener);
2596 }
2597 
2598 /**
2599  * Returns the path matching points.
2600  * @param {Array} points
2601  * @return {GeneralPath}
2602  * @private
2603  */
2604 HomeComponent3D.prototype.getShape = function(points) {
2605   var path = new java.awt.geom.GeneralPath();
2606   path.moveTo(points[0][0], points[0][1]);
2607   for (var i = 1; i < points.length; i++) {
2608     path.lineTo(points[i][0], points[i][1]);
2609   }
2610   path.closePath();
2611   return path;
2612 }
2613 
2614 /**
2615  * Adds a polyline listener to home polylines that updates the children of the given
2616  * <code>group</code>, each time a polyline is added, updated or deleted.
2617  * @param {Group} group
2618  * @private
2619  */
2620 HomeComponent3D.prototype.addPolylineListener = function(group) {
2621   var component3D = this;
2622   this.polylineChangeListener = function(ev) {
2623       var polyline = ev.getSource();
2624       component3D.updateObjects([polyline]);
2625     };
2626   var polylines = this.home.getPolylines();
2627   for (var i = 0; i < polylines.length; i++) {
2628     polylines[i].addPropertyChangeListener(this.polylineChangeListener);
2629   }
2630   this.polylineListener = function(ev) {
2631       var polyline = ev.getItem();
2632       switch (ev.getType()) {
2633         case CollectionEvent.Type.ADD :
2634           component3D.addObject(group, polyline, true, false);
2635           polyline.addPropertyChangeListener(component3D.polylineChangeListener);
2636           break;
2637         case CollectionEvent.Type.DELETE :
2638           component3D.deleteObject(polyline);
2639           polyline.removePropertyChangeListener(component3D.polylineChangeListener);
2640           break;
2641       }
2642     };
2643   this.home.addPolylinesListener(this.polylineListener);
2644 }
2645 
2646 /**
2647  * Adds a dimension line listener to home dimension lines that updates the children of the given
2648  * <code>group</code>, each time a dimension line is added, updated or deleted.
2649  * @param {Group3D} group
2650  * @private
2651  */
2652 HomeComponent3D.prototype.addDimensionLineListener = function(group) {
2653   var component3D = this;
2654   this.dimensionLineChangeListener = function(ev) {
2655       var updatedDimensionLine = ev.getSource();
2656       component3D.updateObjects([updatedDimensionLine]);
2657     };
2658   var dimensionLines = this.home.getDimensionLines();
2659   for (var i = 0; i < dimensionLines.length; i++) {
2660     dimensionLines [i].addPropertyChangeListener(this.dimensionLineChangeListener);
2661   }
2662   this.dimensionLineListener = function(ev) {
2663       var dimensionLine = ev.getItem();
2664       switch (ev.getType()) {
2665         case CollectionEvent.Type.ADD :
2666           component3D.addObject(group, dimensionLine, true, false);
2667           dimensionLine.addPropertyChangeListener(component3D.dimensionLineChangeListener);
2668           break;
2669         case CollectionEvent.Type.DELETE :
2670           component3D.deleteObject(dimensionLine);
2671           dimensionLine.removePropertyChangeListener(component3D.dimensionLineChangeListener);
2672           break;
2673       }
2674     };
2675   this.home.addDimensionLinesListener(this.dimensionLineListener);
2676 }
2677 
2678  /**
2679  * Adds a label listener to home labels that updates the children of the given
2680  * <code>group</code>, each time a label is added, updated or deleted.
2681  * @param {Group3D} group
2682  * @private
2683  */
2684 HomeComponent3D.prototype.addLabelListener = function(group) {
2685   var component3D = this;
2686   this.labelChangeListener = function(ev) {
2687       var label = ev.getSource();
2688       component3D.updateObjects([label]);
2689     };
2690   var labels = this.home.getLabels();
2691   for (var i = 0; i < labels.length; i++) {
2692     labels[i].addPropertyChangeListener(this.labelChangeListener);
2693   }
2694   this.labelListener = function(ev) {
2695       var label = ev.getItem();
2696       switch (ev.getType()) {
2697         case CollectionEvent.Type.ADD :
2698           component3D.addObject(group, label, true, false);
2699           label.addPropertyChangeListener(component3D.labelChangeListener);
2700           break;
2701         case CollectionEvent.Type.DELETE :
2702           component3D.deleteObject(label);
2703           label.removePropertyChangeListener(component3D.labelChangeListener);
2704           break;
2705       }
2706     };
2707   this.home.addLabelsListener(this.labelListener);
2708 }
2709 
2710 /**
2711  * Adds a walls alpha change listener and drawing mode change listener to home
2712  * environment that updates the home scene objects appearance.
2713  * @private
2714  */
2715 HomeComponent3D.prototype.addEnvironmentListeners = function() {
2716   var component3D = this;
2717   this.wallsAlphaListener = function(ev) {
2718       component3D.updateObjects(component3D.home.getWalls());
2719       component3D.updateObjects(component3D.home.getRooms());
2720     };
2721   this.home.getEnvironment().addPropertyChangeListener("WALLS_ALPHA", this.wallsAlphaListener);
2722 }
2723 
2724 /**
2725  * Adds to <code>group</code> a branch matching <code>homeObject</code> at a given <code>index</code>.
2726  * If <code>index</code> is missing or equal to -1, <code>homeObject</code> will be added at the end of the group.
2727  * @param {Group3D} group
2728  * @param {Object}  homeObject
2729  * @param {number}  [index]
2730  * @param {boolean} listenToHomeUpdates
2731  * @param {boolean} waitForLoading
2732  * @private
2733  */
2734 HomeComponent3D.prototype.addObject = function(group, homeObject, index, 
2735                                                listenToHomeUpdates, waitForLoading) {
2736   if (waitForLoading === undefined) {
2737     waitForLoading = listenToHomeUpdates;
2738     listenToHomeUpdates = index;
2739     index = -1;
2740   }
2741   var object3D = this.object3dFactory.createObject3D(this.home, homeObject, waitForLoading);
2742   if (listenToHomeUpdates) {
2743     homeObject.object3D = object3D;
2744     this.homeObjects.push(homeObject);
2745     this.homeObjects3D.push(object3D);
2746   }
2747   if (index === -1) {
2748     group.addChild(object3D);
2749   } else {
2750     group.insertChild(object3D, index);
2751   }
2752   return object3D;
2753 }
2754 
2755 /**
2756  * Adds to <code>group</code> a branch matching <code>homeObject</code> or its children if the piece is a group of furniture.
2757  * @param {Group3D} group
2758  * @param {HomePieceOfFurniture} piece
2759  * @param {boolean} listenToHomeUpdates
2760  * @param {boolean} waitForLoading
2761  * @private
2762  */
2763 HomeComponent3D.prototype.addPieceOfFurniture = function(group, piece, listenToHomeUpdates, waitForLoading) {
2764   if (piece instanceof HomeFurnitureGroup) {
2765     var furniture = piece.getFurniture();
2766     for (var i = 0; i < furniture.length; i++) {
2767       this.addPieceOfFurniture(group, furniture [i], listenToHomeUpdates, waitForLoading);
2768     }
2769   } else {
2770     this.addObject(group, piece, listenToHomeUpdates, waitForLoading);
2771   }
2772 }
2773 
2774 /**
2775  * Detaches from the scene the branch matching <code>homeObject</code>.
2776  * @param {Object}  homeObject
2777  * @private
2778  */
2779 HomeComponent3D.prototype.deleteObject = function(homeObject) {
2780   if (homeObject.object3D) {
2781     homeObject.object3D.detach();
2782     delete homeObject.object3D;
2783     var objectIndex = this.homeObjects.indexOf(homeObject);
2784     this.homeObjects.splice(objectIndex, 1);
2785     this.homeObjects3D.splice(objectIndex, 1);
2786     if (this.homeObjectsToUpdate) {
2787       var index = this.homeObjectsToUpdate.indexOf(homeObject);
2788       if (index >= 0) {
2789         this.homeObjectsToUpdate.splice(index, 1);
2790       }
2791     }
2792   }
2793 }
2794 
2795 /**
2796  * Detaches from the scene the branches matching <code>piece</code> or its children if it's a group.
2797  * @param {HomePieceOfFurniture} piece
2798  * @private
2799  */
2800 HomeComponent3D.prototype.deletePieceOfFurniture = function(piece) {
2801   if (piece instanceof HomeFurnitureGroup) {
2802     var furniture = piece.getFurniture();
2803     for (var i = 0; i < furniture.length; i++) {
2804       this.deletePieceOfFurniture(furniture [i]);
2805     }
2806   } else {
2807     this.deleteObject(piece);
2808   }
2809 }
2810 
2811 /**
2812  * Updates 3D <code>objects</code> later. 
2813  * @param {Array} objects
2814  * @private
2815  */
2816 HomeComponent3D.prototype.updateObjects = function(objects) {
2817   if (this.homeObjectsToUpdate) {
2818     for (var i = 0; i < objects.length; i++) {
2819       var object = objects [i];
2820       if (this.homeObjectsToUpdate.indexOf(object) <= -1) {        
2821         this.homeObjectsToUpdate.push(object);
2822       }
2823     }
2824   } else {
2825     this.homeObjectsToUpdate = objects.slice(0);
2826     // Invoke later the update of objects of homeObjectsToUpdate
2827     setTimeout(
2828         function(component3D) {
2829           for (var i = 0; i < component3D.homeObjectsToUpdate.length; i++) {
2830             var homeObject = component3D.homeObjectsToUpdate [i];
2831             // Check object wasn't deleted since updateObjects call
2832             if (homeObject.object3D) { 
2833               homeObject.object3D.update();
2834             }
2835           }
2836           delete component3D.homeObjectsToUpdate;
2837         }, 0, this);
2838   }
2839   this.approximateHomeBoundsCache = null;
2840   this.homeHeightCache = null;
2841 }
2842 
2843 /**
2844  * Updates 3D objects and furniture groups children, if <code>objects</code> contains some groups.
2845  * @param {Array} objects
2846  * @private
2847  */
2848 HomeComponent3D.prototype.updateObjectsAndFurnitureGroups = function(objects) {
2849   this.updateObjects(objects);
2850   for (var i = 0; i < objects.length; i++) {
2851     var item = objects [i];
2852     if (item instanceof HomeFurnitureGroup) {
2853       this.updateObjects(item.getAllFurniture());
2854     }
2855   }
2856 }
2857 
2858 /**
2859  * Updates walls that may intersect from the given doors or window.
2860  * @param {Array} doorOrWindows
2861  * @private
2862  */
2863 HomeComponent3D.prototype.updateIntersectingWalls = function(doorOrWindows) {
2864   var walls = this.home.getWalls();
2865   var wallCount = 0;
2866   if (this.homeObjectsToUpdate) {
2867     for (var i = 0; i < this.homeObjectsToUpdate.length; i++) {
2868       if (this.homeObjectsToUpdate [i] instanceof Wall) {
2869         wallCount++;
2870       }
2871     }
2872   }
2873 
2874   if (wallCount !== walls.length) {
2875     var updatedWalls = [];
2876     var doorOrWindowBounds = null;
2877     for (var i = 0; i < doorOrWindows.length; i++) {
2878       var doorOrWindow = doorOrWindows [i];
2879       var points = doorOrWindow.getPoints();
2880       if (doorOrWindowBounds === null) {
2881         doorOrWindowBounds = new java.awt.geom.Rectangle2D.Float(points [0][0], points [0][1], 0, 0);
2882       } else {
2883         doorOrWindowBounds.add(points [0][0], points [0][1]);
2884       }
2885       for (var j = 1; j < points.length; j++) {
2886         doorOrWindowBounds.add(points [j][0], points [j][1]);
2887       }
2888     }
2889     // Search walls that intersect approximative bounds
2890     for (var i = 0; i < walls.length; i++) {
2891       var wall = walls [i];
2892       if (wall.intersectsRectangle(doorOrWindowBounds.getX(), doorOrWindowBounds.getY(),
2893           doorOrWindowBounds.getX() + doorOrWindowBounds.getWidth(),
2894           doorOrWindowBounds.getY() + doorOrWindowBounds.getHeight())) {
2895         updatedWalls.push(wall);
2896       }
2897     }
2898     this.updateObjects(updatedWalls);
2899   }
2900 }
2901 
2902 /**
2903  * Updates <code>wall</code> geometry,
2904  * and the walls at its end or start.
2905  * @param {Wall} wall
2906  * @private
2907  */
2908 HomeComponent3D.prototype.updateWall = function(wall) {
2909   var wallsToUpdate = [];
2910   wallsToUpdate.push(wall);
2911   if (wall.getWallAtStart() != null) {
2912     wallsToUpdate.push(wall.getWallAtStart());
2913   }
2914   if (wall.getWallAtEnd() != null) {
2915     wallsToUpdate.push(wall.getWallAtEnd());
2916   }
2917   this.updateObjects(wallsToUpdate);
2918 }
2919 
2920 
2921 /**
2922  * A factory able to create instances of {@link Object3DBranch} class.
2923  * @param {UserPreferences} [preferences]
2924  * @constructor
2925  * @author Emmanuel Puybaret
2926  */
2927 function Object3DBranchFactory(preferences) {
2928   if (preferences !== undefined) {
2929     this.preferences = preferences;
2930   }
2931 }
2932 
2933 /**
2934  * Returns the 3D object matching a given <code>item</code>.
2935  * @param {Home} home
2936  * @param {Object} item
2937  * @param {boolean|function} waitForLoading 
2938  * @return {Object3DBranch} an instance of a subclass of {@link Object3DBranch}
2939  */
2940 Object3DBranchFactory.prototype.createObject3D = function(home, item, waitForLoading) {
2941   if (item instanceof HomePieceOfFurniture) {
2942     return new HomePieceOfFurniture3D(item, home, this.preferences, waitForLoading);
2943   } else if (item instanceof Wall) {
2944     return new Wall3D(item, home, this.preferences, waitForLoading);
2945   } else if (item instanceof Room) {
2946     return new Room3D(item, home, this.preferences, false, waitForLoading);
2947   } else if (item instanceof Polyline) {
2948     return new Polyline3D(item, home, this.preferences, waitForLoading);
2949   } else if (item instanceof DimensionLine) {
2950     return new DimensionLine3D(item, home, this.preferences, waitForLoading);
2951   } else if (item instanceof Label) {
2952      return new Label3D(item, home, this.preferences, waitForLoading);
2953   } else {
2954     return new Group3D();
2955   }  
2956 }
2957