1 /*
  2  * SweetHome3DJSApplication.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <[email protected]>
  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 SweetHome3D.js
 22 // Requires HomeRecorder.js
 23 // Requires UserPreferences.js
 24 // Requires HomePane.js
 25 // Requires JSViewFactory.js
 26 
 27 /**
 28  * Creates a home controller handling savings for local files.
 29  * @param {Home} [home] the home controlled by this controller
 30  * @param {HomeApplication} [application] 
 31  * @param {ViewFactory} [viewFactory]
 32  * @constructor
 33  * @author Emmanuel Puybaret
 34  * @ignore
 35  */
 36 function LocalFileHomeController(home, application, viewFactory) {
 37   HomeController.call(this, home, application, viewFactory);  
 38 }
 39 LocalFileHomeController.prototype = Object.create(HomeController.prototype);
 40 LocalFileHomeController.prototype.constructor = LocalFileHomeController;
 41 
 42 /**
 43  * Creates a new home after closing the current home.
 44  */
 45 LocalFileHomeController.prototype.newHome = function() {
 46   var controller = this;
 47   var newHomeTask = function() {
 48       controller.close();
 49       controller.application.addHome(controller.application.createHome());
 50     };
 51 
 52   if (this.home.isModified() || this.home.isRecovered()) {
 53     this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 
 54         function(save) {
 55           if (save) {
 56             controller.save(newHomeTask);
 57           } else {
 58             newHomeTask();
 59           } 
 60         });
 61   } else {
 62     newHomeTask();
 63   }
 64 }
 65 
 66 /**
 67  * Opens a home chosen by the user.
 68  */
 69 LocalFileHomeController.prototype.open = function() {
 70   var controller = this;
 71   var openHome = function(homeName) {
 72       var preferences = this.application.getUserPreferences();
 73       var openingTaskDialog = new JSDialog(preferences, 
 74           preferences.getLocalizedString("ThreadedTaskPanel", "threadedTask.title"), 
 75           preferences.getLocalizedString("HomeController", "openMessage"), { size: "small" });
 76       openingTaskDialog.findElement(".dialog-cancel-button").style = "display: none";
 77 
 78       var fileInput = document.createElement("input");
 79       fileInput.setAttribute("style", "display: none");
 80       fileInput.setAttribute("type", "file");
 81       document.body.appendChild(fileInput);  
 82       fileInput.addEventListener("change", function(ev) {
 83           document.body.removeChild(fileInput);
 84           if (this.files[0]) {
 85             openingTaskDialog.displayView();
 86             var file = this.files[0];
 87             setTimeout(function() {
 88                 var homeName = file.name.substring(file.name.indexOf("/") + 1);
 89                 controller.application.getHomeRecorder().readHome(URL.createObjectURL(file), {
 90                     homeLoaded: function(home) {
 91                       // Do not set home name because file name may have been altered automatically by browser when saved
 92                       controller.close();
 93                       openingTaskDialog.close(); 
 94                       controller.application.addHome(home);
 95                     },
 96                     homeError: function(error) {
 97                       openingTaskDialog.close(); 
 98                       var message = preferences.
 99                           getLocalizedString("HomeController", "openError", homeName) + "\n" + error;
100                       console.log(error);
101                       alert(message);
102                     }
103                   });
104               }, 100);
105           }
106         }); 
107       fileInput.click();
108     };
109 
110   if (this.home.isModified() || this.home.isRecovered()) {
111     this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 
112         function(save) {
113           if (save) {
114             controller.save(openHome);
115           } else {
116             openHome();
117           } 
118         });
119   } else {
120     openHome();
121   }
122 }
123 
124 /**
125  * Saves the home managed by this controller. If home name doesn't exist,
126  * this method will act as {@link #saveAs() saveAs} method.
127  * @param {function} [postSaveTask]
128  */
129 LocalFileHomeController.prototype.save = function(postSaveTask) {
130   if (this.home.getName() == null
131       || (this.application.configuration !== undefined && this.home.getName() === this.application.configuration.defaultHomeName)) {
132     this.saveAs(postSaveTask);
133   } else {
134     var preferences = this.application.getUserPreferences();
135     var savingTaskDialog = new JSDialog(preferences, 
136         preferences.getLocalizedString("ThreadedTaskPanel", "threadedTask.title"), 
137         preferences.getLocalizedString("HomeController", "saveMessage"), 
138           { 
139             size: "small", 
140             disposer: function(dialog) { 
141               if (dialog.writingOperation !== undefined) { 
142                 dialog.writingOperation.abort(); 
143               } 
144             } 
145           });
146     if (this.application.getHomeRecorder().configuration && this.application.getHomeRecorder().configuration.writeHomeWithWorker) {
147       savingTaskDialog.findElement(".dialog-cancel-button").innerHTML = 
148           ResourceAction.getLocalizedLabelText(preferences, "ThreadedTaskPanel", "cancelButton.text");
149     } else {
150      savingTaskDialog.findElement(".dialog-cancel-button").style = "display: none";
151     }
152     savingTaskDialog.displayView();
153     
154     var controller = this;
155     var homeExtension = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension");   // .sh3d
156     var homeExtension2 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension2"); // .sh3x
157     var homeName = controller.home.getName().replace(homeExtension, homeExtension2);
158     setTimeout(function() {
159         savingTaskDialog.writingOperation = controller.application.getHomeRecorder().writeHome(controller.home, homeName, {
160             homeSaved: function(home, blob) {
161               delete savingTaskDialog.writingOperation;
162               savingTaskDialog.close(); 
163               if (navigator.msSaveOrOpenBlob !== undefined) {
164                 navigator.msSaveOrOpenBlob(blob, homeName);
165               } else {
166                 var downloadLink = document.createElement('a');
167                 downloadLink.setAttribute("style", "display: none");
168                 downloadLink.setAttribute("href", URL.createObjectURL(blob));
169                 downloadLink.setAttribute("download", homeName);
170                 document.body.appendChild(downloadLink);
171                 downloadLink.click();
172                 setTimeout(function() {
173                     document.body.removeChild(downloadLink);
174                     URL.revokeObjectURL(downloadLink.getAttribute("href"));
175                   }, 500);
176               }
177               home.setModified(false);
178               home.setRecovered(false);
179               if (postSaveTask !== undefined) {
180                 postSaveTask();
181               }
182             },
183             homeError: function(status, error) {
184               savingTaskDialog.close(); 
185               console.log(status + " " + error);
186               new JSDialog(preferences, 
187                   preferences.getLocalizedString("HomePane", "error.title"),
188                   preferences.getLocalizedString("HomeController", "saveError", homeName, status + "<br>" + error),  
189                  { size: "small" }).displayView(); 
190             }
191           });
192       }, 200); // Add a little delay to ensure savingTaskDialog is displayed immediately and when no worker started
193   }
194 }
195 
196 /**
197  * Saves the home managed by this controller with a different name.
198  * @param {function} [postSaveTask]
199  */
200 LocalFileHomeController.prototype.saveAs = function(postSaveTask) {
201   var preferences = this.application.getUserPreferences();
202   var message = preferences.getLocalizedString("AppletContentManager", "showSaveDialog.message");
203   var homeName = prompt(message);
204   if (homeName != null && homeName.length > 0) {
205     var homeExtension2 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension2"); // .sh3x
206     this.home.setName(homeName + (homeName.indexOf('.') < 0 ? homeExtension2 : ""));
207     this.save(postSaveTask);    
208   }
209 }
210 
211 /**
212  * Removes home from application homes list.
213  */
214 LocalFileHomeController.prototype.close = function() {
215   this.home.setRecovered(false);
216   this.application.deleteHome(this.home);  
217   this.getView().dispose();
218 }
219 
220 
221 /**
222  * Creates a home controller handling savings from user interface.
223  * @param {Home} [home] the home controlled by this controller
224  * @param {HomeApplication} [application] 
225  * @param {ViewFactory} [viewFactory]
226  * @constructor
227  * @author Emmanuel Puybaret
228  * @ignore
229  */
230 function DirectRecordingHomeController(home, application, viewFactory) {
231   HomeController.call(this, home, application, viewFactory);  
232 }
233 DirectRecordingHomeController.prototype = Object.create(HomeController.prototype);
234 DirectRecordingHomeController.prototype.constructor = DirectRecordingHomeController;
235 
236 /**
237  * Creates a new home after saving and closing the current home.
238  */
239 DirectRecordingHomeController.prototype.newHome = function() {
240   var controller = this;
241   var newHomeTask = function() {
242       controller.close();
243       controller.application.addHome(controller.application.createHome());
244     };
245 
246   if (this.home.isModified() || this.home.isRecovered()) {
247     this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 
248         function(save) {
249           if (save) {
250             controller.save(newHomeTask);
251           } else {
252             newHomeTask();
253           } 
254         });
255   } else {
256     newHomeTask();
257   }
258 }
259 
260 /**
261  * Opens a home after saving and deleting the current home.
262  */
263 DirectRecordingHomeController.prototype.open = function() {
264   var controller = this;
265   var preferences = controller.application.getUserPreferences();
266   var readTask = function(homeName) {
267       if (homeName != null && homeName.length > 0) {
268         controller.application.getHomeRecorder().readHome(homeName, 
269           {
270             homeLoaded: function(home) {
271               home.setName(homeName);
272               controller.close();
273               controller.application.addHome(home);
274             },
275             homeError: function(error) {
276               var message = preferences.getLocalizedString("HomeController", "openError", homeName) + "\n" + error;
277               console.log(error);
278               alert(message);
279             },
280             progression: function(part, info, percentage) {
281             }
282           });
283       }
284     };
285   var selectHome = function() {
286       var request = controller.application.getHomeRecorder().getAvailableHomes({
287           availableHomes: function(homes) {
288             if (homes.length == 0) {
289               var message = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.noAvailableHomes");
290               alert(message);
291             } else {
292               var html = 
293                 '  <div class="column1">' + 
294                 '    <div>@{AppletContentManager.showOpenDialog.message}</div>' + 
295                 '    <div class="home-list"></div>' + 
296                 '  </div>';
297               var fileDialog = new JSDialog(preferences, "@{FileContentManager.openDialog.title}", html, 
298                 {
299                   size: "small", 
300                   applier: function(dialog) {
301                     var selectedItem = fileDialog.findElement(".selected");
302                     if (selectedItem != null) {
303                       readTask(selectedItem.innerText);
304                     }
305                   },
306                 });
307               fileDialog.getHTMLElement().classList.add("open-dialog");
308               var okButton = fileDialog.findElement(".dialog-ok-button");
309               okButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.open");
310               okButton.disabled = true;
311               var cancelButton = fileDialog.findElement(".dialog-cancel-button");
312               cancelButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.cancel");
313               var deleteButton = document.createElement("button");
314               deleteButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.delete");
315               deleteButton.disabled = true;
316               cancelButton.parentElement.insertBefore(deleteButton, cancelButton);
317               var homeList = fileDialog.findElement(".home-list");
318                   
319               for (var i = 0; i < homes.length; i++) {
320                 var item = document.createElement("div");
321                 item.classList.add("item"); 
322                 item.innerHTML = homes [i];
323                 homeList.appendChild(item);
324               }
325               
326               var items = homeList.childNodes;
327               fileDialog.registerEventListener(items, "click", function(ev) {
328                   for (var i = 0; i < items.length; i++) {
329                     if (ev.target == items [i]) {
330                       items [i].classList.add("selected");
331                       okButton.disabled = false;
332                       deleteButton.disabled = ev.target.innerHTML == controller.home.getName();
333                     } else {
334                       items [i].classList.remove("selected");
335                     }
336                   }
337                 });
338               fileDialog.registerEventListener(items, "dblclick", function() {
339                   fileDialog.validate();
340                 });
341               fileDialog.registerEventListener(deleteButton, "click", function(ev) {
342                   var item = fileDialog.findElement(".selected");
343                   controller.confirmDeleteHome(item.innerText, function() {
344                       controller.application.getHomeRecorder().deleteHome(item.innerText, {
345                           homeDeleted: function() {
346                             item.remove();
347                             okButton.disabled = true;
348                             deleteButton.disabled = true;
349                           },
350                           homeError: function(status, error) {
351                             var message = preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.errorMessage", item.innerText);
352                             console.log(message + " : " + error); 
353                             alert(message);
354                           }
355                         });
356                     });
357                 });
358               fileDialog.displayView();
359             }
360           }, 
361           homesError: function(status, error) {
362             var message = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.availableHomesError");
363             console.log(message + " : " + error); 
364             alert(message);
365           }
366         });
367         
368       if (request == null) {
369         var message = preferences.getLocalizedString("AppletContentManager", "showOpenDialog.message");
370         readTask(prompt(message));
371       }
372     };  
373   
374   if (this.home.isModified() || this.home.isRecovered()) {
375     this.getView().confirmSave(this.application.configuration === undefined || this.home.getName() !== this.application.configuration.defaultHomeName ? this.home.getName() : null, 
376         function(save) {
377           if (save) {
378             controller.save(selectHome);
379           } else {
380             selectHome();
381           } 
382         });
383   } else {
384     selectHome();
385   }
386 }
387 
388 /**
389  * Saves the home managed by this controller. If home name doesn't exist,
390  * this method will act as {@link #saveAs() saveAs} method.
391  * @param {function} [postSaveTask]
392  */
393 DirectRecordingHomeController.prototype.save = function(postSaveTask) {
394   if (this.home.getName() == null
395       || (this.application.configuration !== undefined && this.home.getName() === this.application.configuration.defaultHomeName)) {
396     this.saveAs(postSaveTask);
397   } else {
398     var preferences = this.application.getUserPreferences();
399     var savingTaskDialog = new JSDialog(preferences, 
400         preferences.getLocalizedString("ThreadedTaskPanel", "threadedTask.title"), 
401         preferences.getLocalizedString("HomeController", "saveMessage"), 
402           { 
403             size: "small", 
404             disposer: function(dialog) { 
405               if (dialog.writingOperation !== undefined) { 
406                 dialog.writingOperation.abort(); 
407               } 
408             } 
409           });
410     savingTaskDialog.findElement(".dialog-cancel-button").innerHTML = 
411         ResourceAction.getLocalizedLabelText(preferences, "ThreadedTaskPanel", "cancelButton.text");
412     savingTaskDialog.displayView();
413     
414     var controller = this;
415     savingTaskDialog.writingOperation = this.application.getHomeRecorder().writeHome(this.home, this.home.getName(), { 
416         homeSaved: function(home) { 
417           delete savingTaskDialog.writingOperation;
418           savingTaskDialog.close(); 
419           home.setModified(false);
420           home.setRecovered(false);
421           if (postSaveTask !== undefined) {
422             postSaveTask();
423           }
424         }, 
425         homeError: function(status, error) { 
426           savingTaskDialog.close();
427           new JSDialog(preferences, 
428               preferences.getLocalizedString("HomePane", "error.title"),
429               preferences.getLocalizedString("HomeController", "saveError", [controller.home.getName(), error]),  
430              { size: "small" }).displayView(); 
431         } 
432       }); 
433   }
434 }
435 
436 /**
437  * Saves the home managed by this controller with a different name.
438  * @param {function} [postSaveTask]
439  */
440 DirectRecordingHomeController.prototype.saveAs = function(postSaveTask) {
441   var preferences = this.application.getUserPreferences();
442   var message = preferences.getLocalizedString("AppletContentManager", "showSaveDialog.message");
443   var homeName = prompt(message, this.home.getName() != null 
444       && (this.application.configuration === undefined 
445           || this.home.getName() !== this.application.configuration.defaultHomeName) 
446       ? this.home.getName() 
447       : undefined);
448  
449   if (homeName != null && homeName.length > 0) {
450     var controller = this;
451     var request = controller.application.getHomeRecorder().getAvailableHomes({
452         availableHomes: function(homeNames) {
453           var homeExists = false;
454           for (var i = 0; i < homeNames.length; i++) {
455             if (homeName == homeNames [i]) {
456               homeExists = true;
457             }
458           }
459           if (!homeExists) {
460             controller.home.setName(homeName);
461             controller.save(postSaveTask);    
462           } else {
463             var message = preferences.getLocalizedString("FileContentManager", "confirmOverwrite.message", homeName).replace(/\<br\>/g, " ");
464             var confirmOverwriteDialog = new JSDialog(preferences, 
465                 preferences.getLocalizedString("FileContentManager", "confirmOverwrite.title"), 
466                 message + "</font>", 
467                 { 
468                   size: "small", 
469                   applier: function() {
470                     controller.home.setName(homeName);
471                     controller.save(postSaveTask);
472                   }
473                 });
474             confirmOverwriteDialog.findElement(".dialog-ok-button").innerHTML = 
475                 preferences.getLocalizedString("FileContentManager", "confirmOverwrite.overwrite");
476             var cancelButton = confirmOverwriteDialog.findElement(".dialog-cancel-button");
477             cancelButton.innerHTML = preferences.getLocalizedString("FileContentManager", "confirmOverwrite.cancel");
478             confirmOverwriteDialog.displayView();
479           }
480         }, 
481         homesError: function(status, error) {
482           var message = preferences.getLocalizedString("AppletContentManager", "showSaveDialog.checkHomeError");
483           console.log(message + " : " + error); 
484           alert(message);
485         }
486       });
487         
488     if (request == null) {
489       this.home.setName(homeName);
490       this.save(postSaveTask);    
491     };  
492   }
493 }
494 
495 /**
496  * Removes home from application homes list.
497  */
498 DirectRecordingHomeController.prototype.close = function() {
499   this.home.setRecovered(false);
500   this.application.deleteHome(this.home);  
501   this.getView().dispose();
502 }
503 
504 /**
505  * Displays a dialog that lets user choose whether he wants to delete
506  * a home or not, then calls <code>confirm</code>.
507  * @param {function} confirm 
508  * @private
509  */
510 DirectRecordingHomeController.prototype.confirmDeleteHome = function(homeName, confirm) {
511   var preferences = this.application.getUserPreferences();
512   var message = preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.message", homeName).replace(/\<br\>/g, " ");
513   var confirmDeletionDialog = new JSDialog(preferences, 
514       preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.title"), 
515       message + "</font>", 
516       { 
517         size: "small", 
518         applier: function() {
519           confirm();
520         }
521       });
522   confirmDeletionDialog.findElement(".dialog-ok-button").innerHTML = 
523       preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.delete");
524   var cancelButton = confirmDeletionDialog.findElement(".dialog-cancel-button");
525   cancelButton.innerHTML = preferences.getLocalizedString("AppletContentManager", "confirmDeleteHome.cancel");
526   confirmDeletionDialog.displayView();
527 }
528 
529 
530 /**
531  * <code>HomeApplication</code> implementation for JavaScript.
532  * @param {{furnitureCatalogURLs: string[],
533  *          furnitureResourcesURLBase: string,
534  *          texturesCatalogURLs: string[],
535  *          texturesResourcesURLBase: string,
536  *          readHomeURL: string,
537  *          writeHomeEditsURL|writeHomeURL: string,
538  *          closeHomeURL: string,
539  *          writeResourceURL: string,
540  *          readResourceURL: string,
541  *          writePreferencesURL: string,
542  *          readPreferencesURL: string,
543  *          writePreferencesResourceURL: string,
544  *          readPreferencesResourceURL: string,
545  *          pingURL: string,
546  *          autoWriteDelay: number,
547  *          trackedHomeProperties: string[],
548  *          autoWriteTrackedStateChange: boolean,
549  *          defaultUserLanguage: string,
550  *          writeCacheResourceURL: string,
551  *          readCacheResourceURL: string,
552  *          listCacheResourcesURL: string,
553  *          listHomesURL: string,
554  *          deleteHomeURL: string,
555  *          autoRecovery: boolean,
556  *          autoRecoveryDatabase: string,
557  *          autoRecoveryObjectstore: string,
558  *          silentAutoRecovery: boolean,
559  *          compressionLevel: number,
560  *          includeAllContent: boolean,
561  *          writeDataType: string,
562  *          writeHomeWithWorker: boolean, 
563  *          defaultHomeName: string,
564  *          writingObserver: {writeStarted: Function, 
565  *                            writeSucceeded: Function, 
566  *                            writeFailed: Function, 
567  *                            connectionFound: Function, 
568  *                            connectionLost: Function}}  [configuration] 
569  *              the URLs of resources and services required on server
570  *              (if undefined, will use local files for testing).
571  *              If writePreferencesResourceURL / readPreferencesResourceURL is missing,
572  *              writeResourceURL / readResourceURL will be used.
573  *              If writeHomeEditsURL and readHomeURL are missing, application recorder will be 
574  *              an instance of <code>HomeRecorder</code>.
575  *              If writeHomeEditsURL is missing, application recorder will be 
576  *              an instance of <code>DirectHomeRecorder</code>.
577  *              Auto recovery not available for incremental recorder.
578  * @constructor
579  * @author Emmanuel Puybaret
580  * @author Renaud Pawlak
581  */
582 function SweetHome3DJSApplication(configuration) {
583   HomeApplication.call(this);
584   this.homeControllers = [];
585   this.configuration = configuration;
586   var application = this;
587   this.addHomesListener(function(ev) {
588       if (ev.getType() == CollectionEvent.Type.ADD) {
589         var homeController = application.createHomeController(ev.getItem());
590         application.homeControllers.push(homeController); 
591         if (application.getHomeRecorder() instanceof IncrementalHomeRecorder) {
592           application.getHomeRecorder().addHome(ev.getItem(), homeController);
593         }
594         homeController.getView();
595       } else if (ev.getType() == CollectionEvent.Type.DELETE) {
596         application.homeControllers.splice(ev.getIndex(), 1); 
597         if (application.getHomeRecorder() instanceof IncrementalHomeRecorder) {
598           application.getHomeRecorder().removeHome(ev.getItem());
599         }
600       }
601     });
602     
603   if (this.configuration !== undefined
604       && this.configuration.autoRecovery 
605       && this.configuration.writeHomeEditsURL === undefined) {
606     setTimeout(function() {
607         // Launch auto recovery manager
608         application.autoRecoveryManager = new AutoRecoveryManager(application);
609       });
610   }  
611 }
612 SweetHome3DJSApplication.prototype = Object.create(HomeApplication.prototype);
613 SweetHome3DJSApplication.prototype.constructor = SweetHome3DJSApplication;
614 
615 SweetHome3DJSApplication.prototype.getVersion = function() {
616   return "7.5";
617 }
618 
619 SweetHome3DJSApplication.prototype.getHomeRecorder = function() {
620   if (!this.homeRecorder) {
621     this.homeRecorder = this.configuration === undefined || this.configuration.readHomeURL === undefined
622       ? new HomeRecorder(this.configuration)
623       : (this.configuration.writeHomeEditsURL !== undefined
624           ? new IncrementalHomeRecorder(this, this.configuration)
625           : new DirectHomeRecorder(this.configuration));
626   }
627   return this.homeRecorder;
628 }
629 
630 SweetHome3DJSApplication.prototype.getUserPreferences = function() {
631   if (this.preferences == null) {
632     if (this.configuration === undefined) {
633       this.preferences = new DefaultUserPreferences();
634     } else {
635       this.preferences = new RecordedUserPreferences(this.configuration);
636     }
637   }
638   return this.preferences;
639 }
640 
641 SweetHome3DJSApplication.prototype.createHome = function() {
642   var home = HomeApplication.prototype.createHome.call(this);
643   if (this.configuration !== undefined && this.configuration.defaultHomeName !== undefined) {
644     home.setName(this.configuration.defaultHomeName);
645   }
646   return home;
647 }
648 
649 /**
650  * Returns the view factory which will create the views associated to their controllers. 
651  * @return {Object}
652  */
653 SweetHome3DJSApplication.prototype.getViewFactory = function() {
654   if (this.viewFactory == null) {
655     this.viewFactory = new JSViewFactory(this);
656   }
657   return this.viewFactory;
658 }
659 
660 /**
661  * Create the <code>HomeController</code> which controls the given <code>home</code>.
662  * @param {Home} home
663  */
664 SweetHome3DJSApplication.prototype.createHomeController = function(home) {
665   return this.configuration === undefined || this.configuration.readHomeURL === undefined
666       ? new LocalFileHomeController(home, this, this.getViewFactory())
667       : (this.configuration.writeHomeEditsURL !== undefined
668           ? new HomeController(home, this, this.getViewFactory())
669           : new DirectRecordingHomeController(home, this, this.getViewFactory()));
670 }
671 
672 /**
673  * Returns the <code>HomeController</code> associated to the given <code>home</code>.
674  * @return {HomeController}
675  */
676 SweetHome3DJSApplication.prototype.getHomeController = function(home) {
677   return this.homeControllers[this.getHomes().indexOf(home)];
678 }
679 
680 /**
681  * Manager able to automatically save open homes in recovery database with a timer.
682  * The delay between two automatic save operations is specified by 
683  * {@link UserPreferences#getAutoSaveDelayForRecovery() auto save delay for recovery}
684  * property.
685  * @constructor
686  * @param {SweetHome3DJSApplication} application
687  * @ignore
688  * @author Emmanuel Puybaret
689  */
690 function AutoRecoveryManager(application) {
691   this.application = application;
692   var autoRecoveryDatabase = "SweetHome3DJS";
693   var autoRecoveryObjectstore = "Recovery";
694   if (application.configuration.autoRecoveryDatabase !== undefined) {
695     autoRecoveryDatabase = application.configuration.autoRecoveryDatabase;
696   }
697   if (application.configuration.autoRecoveryObjectstore !== undefined) {
698     autoRecoveryObjectstore = application.configuration.autoRecoveryObjectstore;
699   }
700   
701   var manager = this;
702   this.autoRecoveryDatabaseUrlBase = "indexeddb://" + autoRecoveryDatabase + "/" + autoRecoveryObjectstore;
703   // Auto recovery recorder stores data in autoRecoveryObjectstore object store of IndexedDB
704   function AutoRecoveryRecorder() {
705      HomeRecorder.call(this, {
706         readHomeURL: manager.autoRecoveryDatabaseUrlBase + "/content?name=%s.recovered", 
707         writeHomeURL: manager.autoRecoveryDatabaseUrlBase + "?keyPathField=name&contentField=content&dateField=date&name=%s.recovered", 
708         readResourceURL: manager.autoRecoveryDatabaseUrlBase + "/content?name=%s",
709         writeResourceURL: manager.autoRecoveryDatabaseUrlBase + "?keyPathField=name&contentField=content&dateField=date&name=%s", 
710         listHomesURL: manager.autoRecoveryDatabaseUrlBase + "?name=(.*).recovered",
711         deleteHomeURL: manager.autoRecoveryDatabaseUrlBase + "?name=%s.recovered",
712         writeHomeWithWorker: true
713       });
714   }
715   AutoRecoveryRecorder.prototype = Object.create(DirectHomeRecorder.prototype);
716   AutoRecoveryRecorder.prototype.constructor = DirectHomeRecorder;
717 
718   // Reuse XML handler of application recorder
719   AutoRecoveryRecorder.prototype.getHomeXMLHandler = function() {
720     return application.getHomeRecorder().getHomeXMLHandler();
721   }
722   
723   // Reuse XML exporter of application recorder
724   AutoRecoveryRecorder.prototype.getHomeXMLExporter = function() {
725     return application.getHomeRecorder().getHomeXMLExporter();
726   }
727 
728   this.autoSaveRecorder = new AutoRecoveryRecorder();
729   this.recoveredHomeNames = [];
730   var homeExtension1 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension");
731   var homeExtension2 = application.getUserPreferences().getLocalizedString("FileContentManager", "homeExtension2");
732   var homeModificationListener = function(ev) {
733       var home = ev.getSource(); 
734       if (!home.isModified()) {
735         home.removePropertyChangeListener("MODIFIED", homeModificationListener);
736         // Delete auto saved in 1s in case user was traversing quickly the undo/redo pile
737         setTimeout(function() {
738             if (!home.isModified()) {
739               manager.deleteRecoveredHome(home.getName());
740             }
741             home.addPropertyChangeListener("MODIFIED", homeModificationListener);
742           }, 1000);
743       }
744     }; 
745   var homeNameModificationListener = function(ev) {
746       manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(ev.getOldValue()), 1);
747       if (!ev.getSource().isRecovered()) {
748         manager.deleteRecoveredHome(ev.getOldValue());
749       }
750       manager.recoveredHomeNames.push(ev.getNewValue());
751     }; 
752   var homesListener = function(ev) {
753       var home = ev.getItem();
754       if (ev.getType() == CollectionEvent.Type.ADD) {
755         manager.autoSaveRecorder.getAvailableHomes({
756             availableHomes: function(homeNames) {
757               if (home.getName() != null) {
758                 manager.recoveredHomeNames.push(home.getName());
759               }
760               var recoveredHome = false;
761               for (var i = 0; i < homeNames.length; i++) {
762                 if (home.getName() != null 
763                     && this.homeNamesEqual(home.getName(), homeNames [i])) {
764                   if (application.configuration.silentAutoRecovery
765                       || confirm(application.getUserPreferences().getLocalizedString("SweetHome3DJSApplication", "confirmRecoverHome"))) {
766                     recoveredHome = true;
767                     manager.autoSaveRecorder.readHome(homeNames [i], {
768                         homeLoaded: function(replacingHome) {
769                           application.removeHomesListener(homesListener);
770                           application.getHomeController(home).close();
771                           var homeName = replacingHome.getName();
772                           replacingHome.setRecovered(true);
773                           replacingHome.addPropertyChangeListener("RECOVERED", function(ev) {
774                               if (!replacingHome.isRecovered()) {
775                                 manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(replacingHome.getName()), 1);
776                                 manager.deleteRecoveredHome(homeName);
777                                 replacingHome.addPropertyChangeListener("MODIFIED", homeModificationListener);
778                               }
779                             });
780                           replacingHome.addPropertyChangeListener("NAME", homeNameModificationListener);
781                           application.addHome(replacingHome);
782                           application.addHomesListener(homesListener);
783                         },
784                         homeError: function(error) {
785                           var message = application.getUserPreferences().
786                               getLocalizedString("HomeController", "openError", home.getName()) + "\n" + error; 
787                           console.log(message);
788                           alert(message);
789                         },
790                       });
791                   } else {
792                     manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(home.getName()), 1);
793                     manager.deleteRecoveredHome(homeNames [i]);
794                   }
795                   break;
796                 }
797               }
798               
799               if (!recoveredHome) {
800                 home.addPropertyChangeListener("MODIFIED", homeModificationListener);
801                 home.addPropertyChangeListener("NAME", homeNameModificationListener);
802                 if (home.isModified()) {
803                   manager.saveRecoveredHomes();
804                   manager.restartTimer();
805                 }
806               }
807             },
808             homesError: function(status, error) {
809               console.log("Couldn't retrieve homes from database : " + status + " " + error); 
810             },
811             homeNamesEqual: function(name1, name2) {
812               // If both names ends by a home extension
813               var name1Extension1Index = name1.indexOf(homeExtension1, name1.length - homeExtension1.length);
814               var name1Extension2Index = name1.indexOf(homeExtension2, name1.length - homeExtension2.length);
815               var name2Extension1Index = name2.indexOf(homeExtension1, name2.length - homeExtension1.length);
816               var name2Extension2Index = name2.indexOf(homeExtension2, name2.length - homeExtension2.length);
817               if ((name1Extension1Index > 0 || name1Extension2Index > 0)
818                   && (name2Extension1Index > 0 || name2Extension2Index > 0)) {
819                 var name1WithoutExtension = name1Extension1Index > 0 
820                     ? name1.substring(0, name1Extension1Index)
821                     : name1.substring(0, name1Extension2Index);
822                 var name2WithoutExtension = name2Extension1Index > 0 
823                     ? name2.substring(0, name2Extension1Index)
824                     : name2.substring(0, name2Extension2Index);
825                 return name1WithoutExtension === name2WithoutExtension;
826               } else {
827                 return name1 === name2;
828               }
829             }
830           });
831       } else if (ev.getType() == CollectionEvent.Type.DELETE
832                  && home.getName() != null) {
833         if (manager.recoveredHomeNames.indexOf(home.getName()) >= 0) {
834           manager.recoveredHomeNames.splice(manager.recoveredHomeNames.indexOf(home.getName()), 1);
835           manager.deleteRecoveredHome(home.getName());
836         }
837         home.removePropertyChangeListener("MODIFIED", homeModificationListener);
838       }
839     };
840   application.addHomesListener(homesListener);
841     
842   this.lastAutoSaveTime = Date.now();
843   application.getUserPreferences().addPropertyChangeListener("AUTO_SAVE_DELAY_FOR_RECOVERY", function(ev) {
844       manager.restartTimer();
845     });
846   this.restartTimer();
847 }
848 
849 /**
850  * Saves now modified document in auto recovery.
851  * @ignore
852  */
853 AutoRecoveryManager.prototype.saveRecoveredHomes = function() {
854   var homes = this.application.getHomes();
855   for (var i = 0; i < homes.length; i++) {
856     var home = homes [i];
857     if (home.getName() != null) {
858       if (home.isModified()) {
859         this.autoSaveRecorder.writeHome(home, home.getName(), {
860             homeSaved: function(home) {
861             },
862             homeError: function(status, error) {
863               console.log("Couldn't save home for recovery : " + status + " " + error); 
864             }
865           });
866       } else if (this.recoveredHomeNames.indexOf(home.getName()) < 0) {
867         this.deleteRecoveredHome(home.getName());
868       }
869     }
870   }
871   this.lastAutoSaveTime = Math.max(this.lastAutoSaveTime, Date.now());
872 }
873 
874 /**
875  * Restarts the timer that regularly saves application homes.
876  * @ignore
877  */
878 AutoRecoveryManager.prototype.restartTimer = function() {
879   var manager = this;
880   this.stopTimer();
881   var autoSaveDelayForRecovery = application.getUserPreferences().getAutoSaveDelayForRecovery();
882   if (autoSaveDelayForRecovery > 0) {
883     var autoSaveTask = function() {
884         if (Date.now() - manager.lastAutoSaveTime > 5000) {
885           manager.saveRecoveredHomes();
886         }
887       };
888     this.timerIntervalId = setInterval(autoSaveTask, autoSaveDelayForRecovery);
889   }
890 }
891 
892 /**
893  * Restarts the timer that regularly saves application homes.
894  * @ignore
895  */
896 AutoRecoveryManager.prototype.stopTimer = function() {
897   if (this.timerIntervalId) {
898     window.clearInterval(this.timerIntervalId);
899     delete this.timerIntervalId;
900   }
901 }
902 
903 /**
904  * Deletes the given home form auto recovery database.
905  * @private
906  */
907 AutoRecoveryManager.prototype.deleteRecoveredHome = function(homeName) {
908   var manager = this;
909   this.autoSaveRecorder.deleteHome(homeName, { 
910       homeDeleted: function() {
911         if (manager.recoveredHomeNames.length == 0) {
912           // Remove all data if no homes are left in Recovery database
913           // except if an opened home was previously recovered (saving it again will make its data necessary)
914           manager.autoSaveRecorder.getAvailableHomes({
915               availableHomes: function(homeNames) {
916                 if (homeNames.length === 0) {
917                   var dummyRecorder = new DirectHomeRecorder({
918                       listHomesURL: manager.autoRecoveryDatabaseUrlBase + "?name=.*",
919                       deleteHomeURL: manager.autoRecoveryDatabaseUrlBase + "?name=%s"
920                     });
921                   dummyRecorder.getAvailableHomes({ 
922                       availableHomes: function(dataNames) {
923                         for (var i = 0; i < dataNames.length; i++) {
924                           dummyRecorder.deleteHome(dataNames [i], { homeDeleted: function() {} });
925                         }
926                       }
927                     });
928                 }  
929               }
930             });
931         }
932       }
933     });      
934 }
935