1 /*
  2  * viewHome.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2016 Emmanuel PUYBARET / eTeks <info@eteks.com>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 /**
 22  * Loads the home from the given URL and displays it in the 3D canvas with <code>canvasId</code>.
 23  * <code>params.navigationPanel</code> may be equal to <code>"none"</code>, <code>"default"</code> 
 24  * or an HTML string which content will replace the default navigation panel. 
 25  * @param {string} canvasId  the value of the id attribute of the 3D canvas 
 26  * @param {string} homeUrl the URL of the home to load and display
 27  * @param onerror  callback called in case of error with an exception as parameter 
 28  * @param onprogression callback with (part, info, percentage) parameters called during the download of the home 
 29  *                      and the 3D models it displays.
 30  * @param {{roundsPerMinute: number, 
 31  *          navigationPanel: string,
 32  *          aerialViewButtonId: string, 
 33  *          virtualVisitButtonId: string, 
 34  *          levelsAndCamerasListId: string,
 35  *          level: string,
 36  *          selectableLevels: string[],
 37  *          camera: string,
 38  *          selectableCameras: string[],
 39  *          activateCameraSwitchKey: boolean}} [params] the ids of the buttons and other information displayed in the user interface. 
 40  *                      If not provided, controls won't be managed if any, no animation and navigation arrows won't be displayed. 
 41  * @return {HomePreviewComponent} the returned object gives access to the loaded {@link Home} instance, 
 42  *                the {@link HomeComponent3D} instance that displays it, the {@link HomeController3D} instance that manages 
 43  *                camera changes and the {@link UserPreferences} in use.             
 44  */
 45 function viewHome(canvasId, homeUrl, onerror, onprogression, params) {
 46   return new HomePreviewComponent(canvasId, homeUrl, onerror, onprogression, params);
 47 }
 48 
 49 /**
 50  * Loads the home from the given URL and displays it in an overlay. 
 51  * Canvas size ratio is 4 / 3 by default. 
 52  * <code>params.navigationPanel</code> may be equal to <code>"none"</code>, <code>"default"</code> 
 53  * or an HTML string which content will replace the default navigation panel. 
 54  * If needed, the id of the created canvas is <code>viewerCanvas</code> and its <code>homePreviewComponent</code> 
 55  * property returns the instance of {@link HomePreviewComponent} associated to it.
 56  * @param {string} homeUrl the URL of the home to display
 57  * @param {{roundsPerMinute: number, 
 58  *          widthByHeightRatio: number,
 59  *          navigationPanel: string,
 60  *          aerialViewButtonText: string, 
 61  *          virtualVisitButtonText: string, 
 62  *          level: string,
 63  *          selectableLevels: string[],
 64  *          camera: string,
 65  *          selectableCameras: string[],
 66  *          activateCameraSwitchKey: boolean, 
 67  *          viewerControlsAdditionalHTML: string,
 68  *          readingHomeText: string, 
 69  *          readingModelText: string,
 70  *          noWebGLSupportError: string,
 71  *          missingHomeXmlEntryError: string}} [params] the texts and other information displayed in the user interface. 
 72  *                      If not provided, there will be no controls, no animation and canvas size ratio will be 4/3 
 73  *                      with no navigation panel. 
 74  */
 75 function viewHomeInOverlay(homeUrl, params) {
 76   var widthByHeightRatio = 4 / 3;
 77   if (params && params.widthByHeightRatio) {
 78     widthByHeightRatio = params.widthByHeightRatio;
 79   }
 80   
 81   // Ensure no two overlays are displayed
 82   hideHomeOverlay();
 83   
 84   var overlayDiv = document.createElement("div");
 85   overlayDiv.setAttribute("id", "viewerOverlay");
 86   overlayDiv.style.position = "absolute";
 87   overlayDiv.style.left = "0";
 88   overlayDiv.style.top = "0";
 89   overlayDiv.style.zIndex = "100";
 90   overlayDiv.style.background = "rgba(127, 127, 127, .5)";
 91     
 92   var bodyElement = document.getElementsByTagName("body").item(0);
 93   bodyElement.insertBefore(overlayDiv, bodyElement.firstChild);
 94 
 95   var homeViewDiv = document.createElement("div");
 96   var divHTML =
 97         '<canvas id="viewerCanvas" class="viewerComponent"  style="background-color: #CCCCCC; border: 1px solid gray; position: absolute; outline: none; touch-action: none" tabIndex="1"></canvas>'
 98       + '<div id="viewerProgressDiv" style="position:absolute; width: 300px; background-color: rgba(128, 128, 128, 0.7); padding: 20px; border-radius: 25px">'
 99       + '  <progress id="viewerProgress"  class="viewerComponent" value="0" max="200" style="width: 300px;"></progress>'
100       + '  <label id="viewerProgressLabel" class="viewerComponent" style="margin-top: 2px; margin-left: 10px; margin-right: 0px; display: block;"></label>'
101       + '</div>';
102   if (params 
103       && (params.aerialViewButtonText && params.virtualVisitButtonText 
104           || params.viewerControlsAdditionalHTML)) {
105     divHTML += '<div id="viewerControls" style="position: absolute; padding: 10px; padding-top: 5px">';
106     if (params.aerialViewButtonText && params.virtualVisitButtonText) {
107       divHTML += 
108             '   <input  id="aerialView" class="viewerComponent" name="cameraType" type="radio" style="visibility: hidden;"/>'
109           + '      <label class="viewerComponent" for="aerialView" style="visibility: hidden;">' + params.aerialViewButtonText + '</label>'
110           + '   <input  id="virtualVisit" class="viewerComponent" name="cameraType" type="radio" style="visibility: hidden;">'
111           + '      <label class="viewerComponent" for="virtualVisit" style="visibility: hidden;">' + params.virtualVisitButtonText + '</label>'
112           + '   <select id="levelsAndCameras" class="viewerComponent" style="visibility: hidden;"></select>';
113     }
114     if (params.viewerControlsAdditionalHTML) {
115       divHTML += params.viewerControlsAdditionalHTML;
116     }
117     divHTML += '</div>';  
118   }
119   homeViewDiv.innerHTML = divHTML;
120   overlayDiv.appendChild(homeViewDiv);
121 
122   // Create close button image
123   var closeButtonImage = new Image();
124   closeButtonImage.src = ZIPTools.getScriptFolder() + "close.png";
125   closeButtonImage.style.position = "absolute";
126   overlayDiv.appendChild(closeButtonImage);
127   
128   overlayDiv.escKeyListener = function(ev) {
129       if (ev.keyCode === 27) {
130         hideHomeOverlay();
131       }
132     };
133   document.addEventListener("keydown", overlayDiv.escKeyListener);
134   closeButtonImage.addEventListener("click", hideHomeOverlay);
135   var mouseActionsListener = {
136       mousePressed : function(ev) {
137         mouseActionsListener.mousePressedInOverlay = true;
138       },
139       mouseClicked : function(ev) {
140         if (mouseActionsListener.mousePressedInOverlay) {
141           delete mouseActionsListener.mousePressedInOverlay;
142           hideHomeOverlay();
143         }
144       }
145     };
146   overlayDiv.addEventListener("mousedown", mouseActionsListener.mousePressed); 
147   overlayDiv.addEventListener("click", mouseActionsListener.mouseClicked); 
148   overlayDiv.addEventListener("touchmove", 
149       function(ev) {
150         ev.preventDefault();
151       });
152   
153   // Place canvas in the middle of the window
154   var windowWidth  = self.innerWidth;
155   var windowHeight = self.innerHeight;
156   var pageWidth = document.documentElement.clientWidth;
157   var pageHeight = document.documentElement.clientHeight;
158   if (bodyElement && bodyElement.scrollWidth) {
159     if (bodyElement.scrollWidth > pageWidth) {
160       pageWidth = bodyElement.scrollWidth;
161     }
162     if (bodyElement.scrollHeight > pageHeight) {
163       pageHeight = bodyElement.scrollHeight;
164     }
165   }
166   var pageXOffset = self.pageXOffset ? self.pageXOffset : 0;
167   var pageYOffset = self.pageYOffset ? self.pageYOffset : 0;
168   
169   overlayDiv.style.height = Math.max(pageHeight, windowHeight) + "px";
170   overlayDiv.style.width = pageWidth <= windowWidth
171       ? "100%"
172       : pageWidth + "px";
173   overlayDiv.style.display = "block";
174 
175   var canvas = document.getElementById("viewerCanvas");
176   if (windowWidth < windowHeight * widthByHeightRatio) {
177     canvas.width = 0.9 * windowWidth;
178     canvas.height = 0.9 * windowWidth / widthByHeightRatio;
179   } else {
180     canvas.height = 0.9 * windowHeight;
181     canvas.width = 0.9 * windowHeight * widthByHeightRatio;
182   }
183   canvas.style.width = canvas.width + "px";
184   canvas.style.height = canvas.height + "px";
185   var canvasLeft = pageXOffset + (windowWidth - canvas.width - 10) / 2;
186   canvas.style.left = canvasLeft + "px";
187   var canvasTop = pageYOffset + (windowHeight - canvas.height - 10) / 2;
188   canvas.style.top = canvasTop + "px";
189       
190   // Place close button at top right of the canvas
191   closeButtonImage.style.left = (canvasLeft + canvas.width - 5) + "px";
192   closeButtonImage.style.top = (canvasTop - 10) + "px";
193   
194   // Place controls below the canvas
195   var controlsDiv = document.getElementById("viewerControls");
196   if (controlsDiv) {
197     controlsDiv.style.left = (canvasLeft - 10) + "px";
198     controlsDiv.style.top = (canvasTop + canvas.height) + "px";
199     controlsDiv.addEventListener("mousedown", 
200         function(ev) {
201           // Ignore in overlay mouse clicks on controls
202           ev.stopPropagation();
203         });
204   }
205   
206   // Place progress in the middle of the canvas
207   var progressDiv = document.getElementById("viewerProgressDiv");
208   progressDiv.style.left = (canvasLeft + (canvas.width - 300) / 2) + "px";
209   progressDiv.style.top = (canvasTop + (canvas.height - 50) / 2) + "px";
210   progressDiv.style.visibility = "visible";
211   
212   var onerror = function(err) {
213       hideHomeOverlay();
214       if (err == "No WebGL") {
215         var errorMessage = "Sorry, your browser doesn't support WebGL.";
216         if (params.noWebGLSupportError) {
217           errorMessage = params.noWebGLSupportError;
218         }
219         alert(errorMessage);
220       } else if (typeof err === "string" && err.indexOf("No Home.xml entry") == 0) {
221         var errorMessage = "Ensure your home file was saved with Sweet Home 3D 5.3 or a newer version.";
222         if (params.missingHomeXmlEntryError) {
223           errorMessage = params.missingHomeXmlEntryError;
224         }
225         alert(errorMessage);        
226       } else {
227         console.log(err.stack);
228         alert("Error: " + (err.message  ? err.constructor.name + " " +  err.message  : err));
229       }
230     };
231   var onprogression = function(part, info, percentage) {
232       var progress = document.getElementById("viewerProgress");
233       if (progress) {
234         var text = null;
235         if (part === HomeRecorder.READING_HOME) {
236           progress.value = percentage * 100;
237           info = info.substring(info.lastIndexOf('/') + 1);
238           text = params && params.readingHomeText
239               ? params.readingHomeText : part;
240         } else if (part === ModelLoader.READING_MODEL) {
241           progress.value = 100 + percentage * 100;
242           if (percentage === 1) {
243             document.getElementById("viewerProgressDiv").style.visibility = "hidden";
244           }
245           text = params && params.readingModelText
246               ? params.readingModelText : part;
247         }
248         
249         if (text !== null) {
250           document.getElementById("viewerProgressLabel").innerHTML = 
251               (percentage ? Math.floor(percentage * 100) + "% " : "") + text + " " + info;
252         }
253       }
254     };
255  
256   // Display home in canvas 3D
257   var homePreviewComponentContructor = HomePreviewComponent;
258   if (params) {
259     if (params.homePreviewComponentContructor) {
260       homePreviewComponentContructor = params.homePreviewComponentContructor;
261     }
262     if (params.aerialViewButtonText && params.virtualVisitButtonText) {
263       canvas.homePreviewComponent = new homePreviewComponentContructor(
264           "viewerCanvas", homeUrl, onerror, onprogression, 
265           {roundsPerMinute : params.roundsPerMinute, 
266            navigationPanel : params.navigationPanel,
267            aerialViewButtonId : "aerialView", 
268            virtualVisitButtonId : "virtualVisit", 
269            levelsAndCamerasListId : "levelsAndCameras",
270            level : params.level,
271            selectableLevels : params.selectableLevels,
272            camera: params.camera,
273            selectableCameras : params.selectableCameras,
274            activateCameraSwitchKey : params.activateCameraSwitchKey});
275     } else {
276       canvas.homePreviewComponent = new homePreviewComponentContructor(
277           "viewerCanvas", homeUrl, onerror, onprogression, 
278           {roundsPerMinute : params.roundsPerMinute,
279            navigationPanel : params.navigationPanel});
280     }
281   } else {
282     canvas.homePreviewComponent = new homePreviewComponentContructor("viewerCanvas", homeUrl, onerror, onprogression);
283   }
284 }
285 
286 /**
287  * Hides the overlay and disposes resources.
288  * @private
289  */
290 function hideHomeOverlay() {
291   var overlayDiv = document.getElementById("viewerOverlay");
292   if (overlayDiv) {
293     // Free caches and remove listeners bound to global objects 
294     var canvas = document.getElementById("viewerCanvas");
295     if (canvas.homePreviewComponent) {
296       canvas.homePreviewComponent.dispose();
297     }
298     ModelManager.getInstance().clear();
299     TextureManager.getInstance().clear();
300     ZIPTools.clear();
301     window.removeEventListener("keydown", overlayDiv.escKeyListener);
302     document.getElementsByTagName("body").item(0).removeChild(overlayDiv);
303   }
304 }
305 
306 
307 /**
308  * Creates a component that loads and displays a home in a 3D canvas.
309  * @param {string} canvasId  the value of the id attribute of the 3D canvas 
310  * @param {string} homeUrl   the URL of the home to load and display
311  * @param onerror  callback called in case of error with an exception as parameter 
312  * @param onprogression callback with (part, info, percentage) parameters called during the download of the home 
313  *                      and the 3D models it displays.
314  * @param {{roundsPerMinute: number, 
315  *          navigationPanel: string,
316  *          aerialViewButtonId: string, 
317  *          virtualVisitButtonId: string, 
318  *          levelsAndCamerasListId: string,
319  *          level: string,
320  *          selectableLevels: string[],
321  *          camera: string,
322  *          selectableCameras: string[],
323  *          activateCameraSwitchKey: boolean}} [params] the ids of the buttons and other information displayed in the user interface. 
324  *                      If not provided, controls won't be managed if any, no animation and navigation arrows won't be displayed. 
325  * @constructor
326  * @author Emmanuel Puybaret
327  */
328 function HomePreviewComponent(canvasId, homeUrl, onerror, onprogression, params) {
329   if (document.getElementById(canvasId)) {
330     var previewComponent = this;
331     this.createHomeRecorder().readHome(homeUrl,
332         {
333           homeLoaded : function(home) {
334             try {
335               var canvas = document.getElementById(canvasId);
336               if (canvas) {
337                 if (params  
338                     && params.navigationPanel != "none"  
339                     && params.navigationPanel != "default") {
340                   // Create class with a getLocalizedString() method that returns the navigationPanel in parameter
341                   function UserPreferencesWithNavigationPanel(navigationPanel) {
342                     DefaultUserPreferences.call(this);
343                     this.navigationPanel = navigationPanel;
344                   }
345                   UserPreferencesWithNavigationPanel.prototype = Object.create(DefaultUserPreferences.prototype);
346                   UserPreferencesWithNavigationPanel.prototype.constructor = UserPreferencesWithNavigationPanel;
347 
348                   UserPreferencesWithNavigationPanel.prototype.getLocalizedString = function(resourceClass, resourceKey, resourceParameters) {
349                     // Return navigationPanel in parameter for the navigationPanel.innerHTML resource requested by HomeComponent3D
350                     if (resourceClass === HomeComponent3D && resourceKey == "navigationPanel.innerHTML") {
351                       return this.navigationPanel;
352                     } else {
353                       return UserPreferences.prototype.getLocalizedString.call(this, resourceClass, resourceKey, resourceParameters);
354                     }
355                   }
356                   previewComponent.preferences = new UserPreferencesWithNavigationPanel(params.navigationPanel);
357                 } else {
358                   previewComponent.preferences = new DefaultUserPreferences();
359                 }
360                 previewComponent.home = home;
361                 previewComponent.controller = new HomeController3D(home, previewComponent.preferences);
362                 // Create component 3D with loaded home
363                 previewComponent.component3D = previewComponent.createComponent3D(
364                     canvasId, home, previewComponent.preferences, previewComponent.controller);
365                 previewComponent.prepareComponent(canvasId, onprogression,
366                     params ? {roundsPerMinute : params.roundsPerMinute, 
367                               navigationPanelVisible : params.navigationPanel && params.navigationPanel != "none",
368                               aerialViewButtonId : params.aerialViewButtonId, 
369                               virtualVisitButtonId : params.virtualVisitButtonId, 
370                               levelsAndCamerasListId : params.levelsAndCamerasListId,
371                               level : params.level,
372                               selectableLevels : params.selectableLevels,
373                               camera : params.camera,
374                               selectableCameras : params.selectableCameras,
375                               activateCameraSwitchKey : params.activateCameraSwitchKey}
376                            : undefined);
377               }
378             } catch (ex) {
379               onerror(ex);
380             }
381           },
382           homeError : function(err) {
383             onerror(err);
384           },
385           progression : onprogression
386         });
387   } else {
388     onerror("No canvas with id equal to " + canvasId);
389   }
390 }
391 
392 /**
393  * Returns the recorder that will load the home from the given URL.
394  * @return {HomeRecorder}
395  * @protected
396  * @ignore
397  */
398 HomePreviewComponent.prototype.createHomeRecorder = function() { 
399   return new HomeRecorder();
400 }
401 
402 /**
403  * Returns the component 3D that will display the given home.
404  * @param {string} canvasId  the value of the id attribute of the 3D canvas  
405  * @return {HomeComponent3D}
406  * @protected
407  * @ignore
408  */
409 HomePreviewComponent.prototype.createComponent3D = function(canvasId) { 
410   return new HomeComponent3D(canvasId, this.getHome(), this.getUserPreferences(), null, this.getController());
411 }
412 
413 /**
414  * Prepares this component and its user interface.
415  * @param {string} canvasId  the value of the id attribute of the 3D canvas 
416  * @param onprogression callback with (part, info, percentage) parameters called during the download of the home 
417  *                      and the 3D models it displays.
418  * @param {{roundsPerMinute: number, 
419  *          navigationPanelVisible: boolean,
420  *          aerialViewButtonId: string, 
421  *          virtualVisitButtonId: string, 
422  *          levelsAndCamerasListId: string,
423  *          level: string,
424  *          selectableLevels: string[],
425  *          camera: string,
426  *          selectableCameras: string[],
427  *          activateCameraSwitchKey: boolean}} [params] the ids of the buttons and other information displayed in the user interface. 
428  *                      If not provided, controls won't be managed if any, no animation and navigation panel won't be displayed. 
429  * @protected
430  * @ignore
431  */
432 HomePreviewComponent.prototype.prepareComponent = function(canvasId, onprogression, params) { 
433   var roundsPerMinute = params && params.roundsPerMinute ? params.roundsPerMinute : 0;
434   this.startRotationAnimationAfterLoading = roundsPerMinute != 0;
435   if (params && typeof params.navigationPanelVisible) {
436     this.getUserPreferences().setNavigationPanelVisible(params.navigationPanelVisible);
437   }
438   var home = this.getHome();
439   if (home.structure) {
440     // Make always all levels visible if walls and rooms structure can be modified
441     home.getEnvironment().setAllLevelsVisible(true);
442   } else {
443     // Make all levels always visible when observer camera is used
444     var setAllLevelsVisibleWhenObserverCamera = function() {
445         home.getEnvironment().setAllLevelsVisible(home.getCamera() instanceof ObserverCamera);
446       };
447     setAllLevelsVisibleWhenObserverCamera();
448     home.addPropertyChangeListener("CAMERA", setAllLevelsVisibleWhenObserverCamera);
449   }
450   home.getEnvironment().setObserverCameraElevationAdjusted(true);
451   
452   this.trackFurnitureModels(onprogression, roundsPerMinute);
453   
454   // Configure camera type buttons and shortcut
455   var previewComponent = this;
456   var cameraTypeButtonsUpdater = function() {
457       previewComponent.stopRotationAnimation();
458       if (params && params.aerialViewButtonId && params.virtualVisitButtonId) {
459         if (home.getCamera() === home.getTopCamera()) {
460           document.getElementById(params.aerialViewButtonId).checked = true;
461         } else {
462           document.getElementById(params.virtualVisitButtonId).checked = true;
463         }
464       }
465     };
466   var toggleCamera = function() {
467       previewComponent.startRotationAnimationAfterLoading = false;
468       home.setCamera(home.getCamera() === home.getTopCamera() 
469           ? home.getObserverCamera() 
470           : home.getTopCamera());
471       cameraTypeButtonsUpdater();
472     };
473   var canvas = document.getElementById(canvasId);
474   if (params === undefined 
475       || params.activateCameraSwitchKey === undefined
476       || params.activateCameraSwitchKey) {
477     canvas.addEventListener("keydown", 
478           function(ev) {
479         if (ev.keyCode === 32) { // Space bar
480           toggleCamera();
481         }
482       });
483   }
484   if (params && params.aerialViewButtonId && params.virtualVisitButtonId) {
485     var aerialViewButton = document.getElementById(params.aerialViewButtonId);
486     aerialViewButton.addEventListener("change", 
487         function() {
488           previewComponent.startRotationAnimationAfterLoading = false;
489           home.setCamera(aerialViewButton.checked 
490               ? home.getTopCamera() 
491               : home.getObserverCamera());
492         });
493     var virtualVisitButton = document.getElementById(params.virtualVisitButtonId);
494     virtualVisitButton.addEventListener("change", 
495         function() {
496           previewComponent.startRotationAnimationAfterLoading = false;
497           home.setCamera(virtualVisitButton.checked 
498               ? home.getObserverCamera() 
499               : home.getTopCamera());
500         });
501     cameraTypeButtonsUpdater();
502     // Make radio buttons and their label visible
503     aerialViewButton.style.visibility = "visible";
504     virtualVisitButton.style.visibility = "visible";
505     var makeLabelVisible = function(buttonId) {
506         var labels = document.getElementsByTagName("label");
507         for (var i = 0; i < labels.length; i++) {
508           if (labels [i].getAttribute("for") == buttonId) {
509             labels [i].style.visibility = "visible";
510           }
511         }
512       }
513     makeLabelVisible(params.aerialViewButtonId);
514     makeLabelVisible(params.virtualVisitButtonId);
515     home.addPropertyChangeListener("CAMERA", 
516         function() {
517           cameraTypeButtonsUpdater();
518           if (home.structure && params && params.levelsAndCamerasListId) {
519             document.getElementById(params.levelsAndCamerasListId).disabled = home.getCamera() === home.getTopCamera();
520           }
521         });
522   } 
523 
524   if (params && params.level) {
525     var levels = home.getLevels();
526     if (levels.length > 0) {
527       for (var i = 0; i < levels.length; i++) {
528         var level = levels [i];
529         if (level.isViewable()
530             && level.getName() == params.level) {
531           home.setSelectedLevel(level);
532           break;
533         }
534       }
535     }
536   }
537   
538   if (params && params.camera) {
539     var cameras = home.getStoredCameras();
540     if (cameras.length > 0) {
541       for (var i = 0; i < cameras.length; i++) {
542         var camera = cameras [i];
543         if (camera.getName() == params.camera) {
544           this.getController().goToCamera(camera);
545           break;
546         }
547       }
548     }
549   }
550   
551   if (params && params.levelsAndCamerasListId) {
552     var levelsAndCamerasList = document.getElementById(params.levelsAndCamerasListId);
553     levelsAndCamerasList.disabled = home.structure !== undefined && home.getCamera() === home.getTopCamera();
554     var levels = home.getLevels();
555     if (levels.length > 0) {
556       for (var i = 0; i < levels.length; i++) {
557         var level = levels [i];
558         if (level.isViewable()
559             && (!params.selectableLevels 
560                 || params.selectableLevels.indexOf(level.getName()) >= 0)) {
561           var option = document.createElement("option");
562           option.text  = level.getName();
563           option.level = level;
564           levelsAndCamerasList.add(option);
565           if (level === home.getSelectedLevel()) {
566             levelsAndCamerasList.selectedIndex = levelsAndCamerasList.options.length - 1;
567           }
568         }
569       }
570     }
571       
572     if (params.selectableCameras !== undefined) {
573       var cameras = home.getStoredCameras();
574       if (cameras.length > 0) {
575         var addSeparator = levelsAndCamerasList.options.length > 0;
576         for (var i = 0; i < cameras.length; i++) {
577           var camera = cameras [i];
578           if (params.selectableCameras.indexOf(camera.getName()) >= 0) {
579             if (addSeparator) {
580               levelsAndCamerasList.add(document.createElement("option"));
581               addSeparator = false;
582             }
583             var option = document.createElement("option");
584             option.text  = camera.getName();
585             option.camera = camera;
586             levelsAndCamerasList.add(option);
587           }
588         }
589       }
590     }
591         
592     if (levelsAndCamerasList.options.length > 1) {
593       var controller = this.getController();
594       levelsAndCamerasList.addEventListener("change", 
595           function() {
596             previewComponent.startRotationAnimationAfterLoading = false;
597             var selectedOption = levelsAndCamerasList.options [levelsAndCamerasList.selectedIndex];
598             if (selectedOption.level !== undefined) {
599               home.setSelectedLevel(selectedOption.level);
600             } else if (selectedOption.camera !== undefined) {
601               controller.goToCamera(selectedOption.camera);
602             }  
603           });
604       levelsAndCamerasList.style.visibility = "visible";
605     }
606   }
607   
608   if (roundsPerMinute) {
609     var controller = this.getController();
610     controller.goToCamera(home.getTopCamera());
611     controller.rotateCameraPitch(Math.PI / 6 - home.getCamera().getPitch());
612     controller.moveCamera(10000);
613     controller.moveCamera(-50);
614     this.clickListener = function(ev) {
615         previewComponent.startRotationAnimationAfterLoading = false;
616         previewComponent.stopRotationAnimation();
617       };
618     canvas.addEventListener("keydown", this.clickListener);
619     if (OperatingSystem.isInternetExplorerOrLegacyEdge()
620         && window.PointerEvent) {
621       // Multi touch support for IE and Edge
622       canvas.addEventListener("pointerdown", this.clickListener);
623       canvas.addEventListener("pointermove", this.clickListener);
624     } else {
625       canvas.addEventListener("mousedown", this.clickListener);
626       canvas.addEventListener("touchstart",  this.clickListener);
627       canvas.addEventListener("touchmove",  this.clickListener);
628     }
629     var elements = this.component3D.getSimulatedKeyElements(document.getElementsByTagName("body").item(0));
630     for (var i = 0; i < elements.length; i++) {
631       if (OperatingSystem.isInternetExplorerOrLegacyEdge()
632           && window.PointerEvent) {
633         elements [i].addEventListener("pointerdown", this.clickListener);
634       } else {
635         elements [i].addEventListener("mousedown", this.clickListener);
636       }
637     }
638     this.visibilityChanged = function(ev) {
639         if (document.visibilityState == "hidden") {
640           previewComponent.stopRotationAnimation();
641         }
642       }
643     document.addEventListener("visibilitychange", this.visibilityChanged);
644     var canvasBounds = canvas.getBoundingClientRect();
645     // Request focus if canvas is fully visible
646     if (canvasBounds.top >= 0 && canvasBounds.bottom <= self.innerHeight) {
647       canvas.focus();
648     }
649   }
650 }
651 
652 /**
653  * Returns the home displayed by this component.
654  * @return {Home}
655  */
656 HomePreviewComponent.prototype.getHome = function() {
657   return this.home;
658 }
659 
660 /**
661  * Returns the component 3D that displays the home of this component.
662  * @return {HomeComponent3D}
663  */
664 HomePreviewComponent.prototype.getComponent3D = function() {
665   return this.component3D;
666 }  
667 
668 /**
669  * Returns the controller that manages changes in the home bound to this component.
670  * @return {HomeController3D}
671  */
672 HomePreviewComponent.prototype.getController = function() {
673   return this.controller;
674 }  
675 
676 /**
677  * Returns the user preferences used by this component.
678  * @return {UserPreferences}
679  */
680 HomePreviewComponent.prototype.getUserPreferences = function() {
681   return this.preferences;
682 }
683 
684 /**
685  * Tracks furniture models loading to dispose unneeded files and data once read.
686  * @private
687  */
688 HomePreviewComponent.prototype.trackFurnitureModels = function(onprogression, roundsPerMinute) {
689   var loadedFurniture = [];
690   var loadedJars = {};
691   var loadedModels = {};
692   var home = this.getHome();
693   var furniture = home.getFurniture();          
694   for (var i = 0; i < furniture.length; i++) { 
695     var piece = furniture [i];
696     var pieces = [];
697     if (piece instanceof HomeFurnitureGroup) {
698       var groupFurniture = piece.getAllFurniture();
699       for (var j = 0; j < groupFurniture.length; j++) {
700         var childPiece = groupFurniture [j];
701         if (!(childPiece instanceof HomeFurnitureGroup)) {
702           pieces.push(childPiece);
703         }
704       }
705     } else {
706       pieces.push(piece);
707     }
708     loadedFurniture.push.apply(loadedFurniture, pieces);
709     for (var j = 0; j < pieces.length; j++) { 
710       var model = pieces [j].getModel();
711       if (model.isJAREntry()) {
712         var jar = model.getJAREntryURL();
713         if (jar in loadedJars) {
714           loadedJars [jar]++;
715         } else {
716           loadedJars [jar] = 1;
717         }
718       }
719       var modelUrl = model.getURL();
720       if (modelUrl in loadedModels) {
721         loadedModels [modelUrl]++;
722       } else {
723         loadedModels [modelUrl] = 1;
724       }
725     }
726   }
727 
728   if (loadedFurniture.length === 0) {
729     onprogression(ModelLoader.READING_MODEL, undefined, 1);
730   } else {
731     // Add an observer that will close ZIP files and free geometries once all models are loaded
732     var modelsCount = 0;
733     var previewComponent = this;
734     for (var i = 0; i < loadedFurniture.length; i++) {
735       var managerCall = function(piece) {
736         ModelManager.getInstance().loadModel(piece.getModel(), false, {
737           modelUpdated : function(modelRoot) {
738             var model = piece.getModel();
739             if (model.isJAREntry()) {
740               var jar = model.getJAREntryURL();
741               if (--loadedJars [jar] === 0) {
742                 ZIPTools.disposeZIP(jar);
743                 delete loadedJars [jar];
744               }
745             }
746             var modelUrl = model.getURL();
747             if (--loadedModels [modelUrl] === 0) {
748               ModelManager.getInstance().unloadModel(model);
749               delete loadedModels [modelUrl];
750             }
751             onprogression(ModelLoader.READING_MODEL, piece.getName(), ++modelsCount / loadedFurniture.length);
752             if (modelsCount === loadedFurniture.length) {
753               // Home and its models fully loaded
754               // Free all other geometries (background, structure...)  
755               previewComponent.component3D.disposeGeometries();
756               loadedFurniture = [];
757               if (previewComponent.startRotationAnimationAfterLoading) {
758                 delete previewComponent.startRotationAnimationAfterLoading;
759                 previewComponent.startRotationAnimation(roundsPerMinute); 
760               }
761             }
762           },        
763           modelError : function(ex) {
764             this.modelUpdated();
765           }
766         });
767       };
768       managerCall(loadedFurniture [i]);
769     }
770   }
771 }
772 
773 /**
774  * Stops animation, removes listeners bound to global objects and clears this component.
775  * This method should be called to free resources in the browser when this component is not needed anymore.
776  */
777 HomePreviewComponent.prototype.dispose = function() {
778   this.stopRotationAnimation();
779   if (this.component3D) {
780     if (this.clickListener) {
781       // Remove listeners bound to global objects
782       document.removeEventListener("visibilitychange", this.visibilityChanged);
783       var elements = this.component3D.getSimulatedKeyElements(document.getElementsByTagName("body").item(0));
784       for (var i = 0; i < elements.length; i++) {
785         if (OperatingSystem.isInternetExplorerOrLegacyEdge()
786             && window.PointerEvent) {
787           elements [i].removeEventListener("pointerdown", this.clickListener);
788         } else {
789           elements [i].removeEventListener("mousedown", this.clickListener);
790         }
791       }
792     }
793     this.component3D.dispose();
794   }
795 }
796 
797 /**
798  * Starts rotation animation.
799  * @param {number} [roundsPerMinute]  the rotation speed in rounds per minute, 1rpm if missing
800  */
801 HomePreviewComponent.prototype.startRotationAnimation = function(roundsPerMinute) {
802   this.roundsPerMinute = roundsPerMinute !== undefined ? roundsPerMinute : 1;
803   if (!this.rotationAnimationStarted) {
804     this.rotationAnimationStarted = true;
805     this.animate();
806   }
807 }
808 
809 /**
810  * @private
811  */
812 HomePreviewComponent.prototype.animate = function() {
813   if (this.rotationAnimationStarted) {
814     var now = Date.now();
815     if (this.lastRotationAnimationTime !== undefined) {
816       var angularSpeed = this.roundsPerMinute * 2 * Math.PI / 60000; 
817       var yawDelta = ((now - this.lastRotationAnimationTime) * angularSpeed) % (2 * Math.PI);
818       yawDelta -= this.home.getCamera().getYaw() - this.lastRotationAnimationYaw;
819       if (yawDelta > 0) {
820         this.controller.rotateCameraYaw(yawDelta);
821       }
822     }
823     this.lastRotationAnimationTime = now;
824     this.lastRotationAnimationYaw = this.home.getCamera().getYaw();
825     var previewComponent = this;
826     requestAnimationFrame(
827         function() {
828           previewComponent.animate();
829         });
830   }
831 }
832 
833 /**
834  * Stops the running rotation animation.
835  */
836 HomePreviewComponent.prototype.stopRotationAnimation = function() {
837   delete this.lastRotationAnimationTime;
838   delete this.lastRotationAnimationYaw;
839   delete this.rotationAnimationStarted;
840 }
841