1 /*
  2  * UserPreferences.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2015 Emmanuel PUYBARET / eTeks <info@eteks.com>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 // Requires core.js
 22 //          LengthUnit.js
 23 //          URLContent.js
 24 
 25 /**
 26  * User preferences.
 27  * @constructor
 28  * @author Emmanuel Puybaret
 29  */
 30 function UserPreferences() {
 31   this.propertyChangeSupport = new PropertyChangeSupport(this);
 32 
 33   this.initSupportedLanguages(UserPreferences.DEFAULT_SUPPORTED_LANGUAGES);
 34   
 35   this.resourceBundles = [];
 36   this.furnitureCatalogResourceBundles = [];
 37   this.texturesCatalogResourceBundles = [];
 38 
 39   /** @type {FurnitureCatalog} */
 40   this.furnitureCatalog = null;
 41   /** @type {TexturesCatalog} */
 42   this.texturesCatalog = null;
 43   /** @type {PatternsCatalog} */
 44   this.patternsCatalog = null;
 45   this.currency = null;
 46   this.valueAddedTaxEnabled = false
 47   this.defaultValueAddedTaxPercentage = null;
 48   /** @type {LengthUnit} */
 49   this.unit = null;
 50   this.furnitureCatalogViewedInTree = false;
 51   this.navigationPanelVisible = true;
 52   this.editingIn3DViewEnabled = false;
 53   this.aerialViewCenteredOnSelectionEnabled = false;
 54   this.observerCameraSelectedAtChange = true;
 55   this.magnetismEnabled = true;
 56   this.rulersVisible = true;
 57   this.gridVisible = true;
 58   this.defaultFontName = null;
 59   this.furnitureViewedFromTop = true;
 60   this.furnitureModelIconSize = 128;
 61   this.roomFloorColoredOrTextured = true;
 62   /** @type {TextureImage} */
 63   this.wallPattern = null;
 64   /** @type {TextureImage}  */
 65   this.newWallPattern = null;
 66   this.newWallThickness = 7.5;
 67   this.newWallHeight = 250;
 68   this.newWallBaseboardThickness = 1;
 69   this.newWallBaseboardHeight = 7;
 70   this.newRoomFloorColor = null;
 71   this.newFloorThickness = 12;
 72   this.autoSaveDelayForRecovery = 0;
 73   this.recentHomes = [];
 74   this.autoCompletionStrings = {};
 75   this.recentColors = [];
 76   this.recentTextures = [];
 77   this.homeExamples = [];
 78 
 79   this.ignoredActionTips = {};
 80 }
 81 
 82 UserPreferences.DEFAULT_SUPPORTED_LANGUAGES = ["bg", "cs", "de", "el", "en", "es", "fr", "it", "ja", "hu", "nl", "pl", "pt", "pt_BR", "ru", "sv", "vi", "zh_CN", "zh_TW"];
 83 
 84 UserPreferences.DEFAULT_TEXT_STYLE = new TextStyle(18);
 85 UserPreferences.DEFAULT_ROOM_TEXT_STYLE = new TextStyle(24);
 86 
 87 /**
 88  * Initializes the supportedLanguage property (and potentially the language property if it has to change)
 89  * @private
 90  */
 91 UserPreferences.prototype.initSupportedLanguages = function(supportedLanguages) {
 92   this.supportedLanguages = supportedLanguages;
 93   // We also initialize the language except if already set and within the supported languages
 94   if (!this.language || this.supportedLanguages.indexOf(this.language) === -1) {
 95     var defaultLocale = Locale.getDefault();
 96     if (defaultLocale === null) {
 97       defaultLocale = "en";
 98     }
 99     this.defaultCountry = "";
100     var defaultLanguage = defaultLocale;
101     if (defaultLocale.indexOf("_") > 0) {
102       this.defaultCountry = defaultLocale.substring(defaultLocale.indexOf("_") + 1, defaultLocale.length);
103       defaultLanguage = this.language 
104           ? this.language.substring(0, this.language.indexOf("_")) 
105           : defaultLocale.substring(0, defaultLocale.indexOf("_"));
106     }
107     // Find closest language among supported languages in Sweet Home 3D
108     // For example, use simplified Chinese even for Chinese users (zh_?) not from China (zh_CN)
109     // unless their exact locale is supported as in Taiwan (zh_TW)
110     for (var i = 0; i < this.supportedLanguages.length; i++) {
111       var supportedLanguage = this.supportedLanguages[i];
112       if (this.defaultCountry != "" && supportedLanguage == defaultLanguage + "_" + this.defaultCountry
113           || this.defaultCountry == "" && supportedLanguage == defaultLanguage) {
114         this.language = supportedLanguage;
115         break; // Found the exact supported language
116       } else if (this.language === undefined
117         && supportedLanguage.indexOf(defaultLanguage) === 0) {
118         this.language = supportedLanguage; // Found a supported language
119       }
120     }
121     // If no language was found, let's use English by default
122     if (this.language === undefined) {
123       this.language = "en";
124     }
125     this.updateDefaultLocale();
126   }
127 }
128 
129 /**
130  * Updates default locale from preferences language.
131  * @private
132  */
133 UserPreferences.prototype.updateDefaultLocale = function() {
134   if (this.language.indexOf("_") !== -1
135       || this.defaultCountry == "") {
136     Locale.setDefault(this.language);
137   } else {
138     Locale.setDefault(this.language + "_" + this.defaultCountry);
139   }
140 }
141 
142 /**
143  * Writes user preferences.
144  */
145 UserPreferences.prototype.write = function() {
146   // Does nothing
147 }
148 
149 /**
150  * Adds the property change <code>listener</code> in parameter to these preferences.
151  * @since 6.4
152  */
153 UserPreferences.prototype.addPropertyChangeListener = function(listener) {
154   this.propertyChangeSupport.addPropertyChangeListener(listener);
155 }
156 
157 /**
158  * Removes the property change <code>listener</code> in parameter from these preferences.
159  * @since 6.4
160  */
161 UserPreferences.prototype.removePropertyChangeListener = function(listener) {
162   this.propertyChangeSupport.removePropertyChangeListener(listener);
163 }
164 
165 /**
166  * Adds the <code>listener</code> in parameter to these preferences to listen
167  * to the changes of the given <code>property</code>.
168  * The listener is a function that will receive in parameter an event of {@link PropertyChangeEvent} class.
169  */
170 UserPreferences.prototype.addPropertyChangeListener = function(property, listener) {
171   this.propertyChangeSupport.addPropertyChangeListener(property, listener);
172 }
173 
174 /**
175  * Removes the <code>listener</code> in parameter from these preferences.
176  */
177 UserPreferences.prototype.removePropertyChangeListener = function(property, listener) {
178   this.propertyChangeSupport.removePropertyChangeListener(property, listener);
179 }
180 
181 /**
182  * Returns the furniture catalog.
183  * @ignore
184  */
185 UserPreferences.prototype.getFurnitureCatalog = function() {
186   return this.furnitureCatalog;
187 }
188 
189 /**
190  * Sets furniture catalog.
191  * @ignore
192  */
193 UserPreferences.prototype.setFurnitureCatalog = function(catalog) {
194   this.furnitureCatalog = catalog;
195 }
196 
197 /**
198  * Returns the textures catalog.
199  * @ignore
200  */
201 UserPreferences.prototype.getTexturesCatalog = function() {
202   return this.texturesCatalog;
203 }
204 
205 /**
206  * Sets textures catalog.
207  * @ignore
208  */
209 UserPreferences.prototype.setTexturesCatalog = function(catalog) {
210   this.texturesCatalog = catalog;
211 }
212 
213 /**
214  * Returns the patterns catalog available to fill plan areas. 
215  */
216 UserPreferences.prototype.getPatternsCatalog = function() {
217   return this.patternsCatalog;
218 }
219 
220 /**
221  * Sets the patterns available to fill plan areas.
222  * @ignore
223  */
224 UserPreferences.prototype.setPatternsCatalog = function(catalog) {
225   this.patternsCatalog = catalog;
226 }
227 
228 /**
229  * Returns the length unit currently in use.
230  * @return {LengthUnit}
231  */
232 UserPreferences.prototype.getLengthUnit = function() {
233   return this.unit;
234 }
235 
236 /**
237  * Changes the unit currently in use, and notifies listeners of this change. 
238  * @param unit one of the values of Unit.
239  */
240 UserPreferences.prototype.setUnit = function(unit) {
241   if (this.unit !== unit) {
242     var oldUnit = this.unit;
243     this.unit = unit;
244     this.propertyChangeSupport.firePropertyChange("UNIT", oldUnit, unit);
245   }
246 }
247 
248 /**
249  * Returns the preferred language to display information, noted with an ISO 639 code
250  * that may be followed by an underscore and an ISO 3166 code.
251  */
252 UserPreferences.prototype.getLanguage = function() {
253   return this.language;
254 }
255 
256 /**
257  * If language can be changed, sets the preferred language to display information, 
258  * changes current default locale accordingly and notifies listeners of this change.
259  * @param language an ISO 639 code that may be followed by an underscore and an ISO 3166 code
260  *            (for example fr, de, it, en_US, zh_CN). 
261  */
262 UserPreferences.prototype.setLanguage = function(language) {
263   if (language != this.language && this.isLanguageEditable()) {
264     var oldLanguage = this.language;
265     this.language = language;
266     // Make it accessible to other localized parts (e.g. LengthUnit)
267     this.updateDefaultLocale();
268     this.resourceBundles = [];
269     this.furnitureCatalogResourceBundles = [];
270     this.texturesCatalogResourceBundles = [];
271     this.propertyChangeSupport.firePropertyChange("LANGUAGE", oldLanguage, language);
272   }
273 }
274 
275 /**
276  * Returns <code>true</code> if the language in preferences can be set.
277  * @return <code>true</code> except if <code>user.language</code> System property isn't writable.
278  * @ignore 
279  */
280 UserPreferences.prototype.isLanguageEditable = function() {
281   return true;
282 }
283 
284 /**
285  * Returns the array of default available languages in Sweet Home 3D.
286  * @return an array of languages_countries ISO representations
287  */
288 UserPreferences.prototype.getDefaultSupportedLanguages = function() {
289   return UserPreferences.DEFAULT_SUPPORTED_LANGUAGES.slice(0);
290 }
291 
292 /**
293  * Returns the array of available languages in Sweet Home 3D including languages in libraries.
294  */
295 UserPreferences.prototype.getSupportedLanguages = function() {
296   return this.supportedLanguages.slice(0);
297 }
298 
299 /**
300  * Returns the array of available languages in Sweet Home 3D.
301  */
302 UserPreferences.prototype.setSupportedLanguages = function(supportedLanguages) {
303   if (this.supportedLanguages != supportedLanguages) {
304     var oldSupportedLanguages = this.supportedLanguages;
305     var oldLanguage = this.language;
306     this.initSupportedLanguages(supportedLanguages.slice(0));
307     this.propertyChangeSupport.firePropertyChange("SUPPORTED_LANGUAGES", oldSupportedLanguages, supportedLanguages);
308     if (oldLanguage != this.language) {
309       this.propertyChangeSupport.firePropertyChange("LANGUAGE", oldLanguage, language);
310     }
311   }
312 }
313 
314 /**
315  * Returns the string matching <code>resourceKey</code> in current language in the 
316  * context of <code>resourceClass</code> or for a resource family if <code>resourceClass</code>
317  * is a string.
318  * If <code>resourceParameters</code> isn't empty the string is considered
319  * as a format string, and the returned string will be formatted with these parameters. 
320  * This implementation searches first the key in a properties file named as 
321  * <code>resourceClass</code>, then if this file doesn't exist, it searches 
322  * the key prefixed by <code>resourceClass</code> name and a dot in a package.properties file 
323  * in the folder matching the package of <code>resourceClass</code>. 
324  * @throws IllegalArgumentException if no string for the given key can be found
325  */
326 UserPreferences.prototype.getLocalizedString = function(resourceClass, resourceKey, resourceParameters) {
327   this.getResourceBundles(resourceClass);
328   if (resourceClass == "DefaultFurnitureCatalog") {
329     return CoreTools.getStringFromKey.apply(null, [this.furnitureCatalogResourceBundles, resourceKey].concat(Array.prototype.slice.call(arguments, 2))); 
330   } else if (resourceClass == "DefaultTexturesCatalog") {
331     return CoreTools.getStringFromKey.apply(null, [this.texturesCatalogResourceBundles, resourceKey].concat(Array.prototype.slice.call(arguments, 2))); 
332   } else {
333     // JSweet-generated code interop: if resourceClass is a constructor, it may contain the Java class full name in __class
334     if (resourceClass.__class) {
335       var resourceClassArray = resourceClass.__class.split('.');
336       resourceClass = resourceClassArray[resourceClassArray.length - 1];
337     }
338     var key = resourceClass + "." + resourceKey;
339     return CoreTools.getStringFromKey.apply(null, [this.resourceBundles, key].concat(Array.prototype.slice.call(arguments, 2))); 
340   } 
341 }
342 
343 /**
344  * Returns the keys of the localized property strings of the given resource family.
345  * @throws IllegalArgumentException if the given resourceFamily is not supported
346  */
347 UserPreferences.prototype.getLocalizedStringKeys = function(resourceFamily) {
348   if (resourceClass == "DefaultFurnitureCatalog") {
349     var keys = {};
350     for (var i = 0; i < resourceBundles.length; i++) {
351       if (resourceBundles[i] != null) {
352         CoreTools.merge(keys,  resourceBundles[i]);
353       }
354     }
355     return Object.getOwnPropertyNames(keys);
356   } else {
357     throw new IllegalArgumentException("unsupported family");
358   }
359 }
360 
361 /**
362  * Returns the resource bundle for the given resource family.
363  */
364 UserPreferences.prototype.getResourceBundles = function(resourceClass) {
365   if (resourceClass == "DefaultFurnitureCatalog") {
366     if (this.furnitureCatalogResourceBundles.length == 0) {
367       this.furnitureCatalogResourceBundles = CoreTools.loadResourceBundles("resources/DefaultFurnitureCatalog", Locale.getDefault());
368     }
369     return this.furnitureCatalogResourceBundles;
370   } else if (resourceClass == "DefaultTexturesCatalog") {
371     if (this.texturesCatalogResourceBundles.length == 0) {
372       this.texturesCatalogResourceBundles = CoreTools.loadResourceBundles("resources/DefaultTexturesCatalog", Locale.getDefault());
373     }
374     return this.texturesCatalogResourceBundles;
375   } else {
376     if (this.resourceBundles.length == 0) {
377       this.resourceBundles = CoreTools.loadResourceBundles("resources/localization", Locale.getDefault());
378     }
379     return this.resourceBundles;
380   } 
381 }
382 
383 /**
384  * Returns the default currency in use, noted with ISO 4217 code, or <code>null</code> 
385  * if prices aren't used in application.
386  * @ignore
387  */
388 UserPreferences.prototype.getCurrency = function() {
389   return this.currency;
390 }
391 
392 /**
393  * Sets the default currency in use.
394  * @ignore
395  */
396 UserPreferences.prototype.setCurrency = function(currency) {
397   if (currency != this.currency) {
398     var oldCurrency = this.currency;
399     this.currency = currency;
400     this.propertyChangeSupport.firePropertyChange("CURRENCY", oldCurrency, currency);
401   }
402 }
403 
404 /**
405  * Returns <code>true</code> if Value Added Tax should be taken in account in prices.
406  * @since 6.0
407  * @ignore
408  */
409 UserPreferences.prototype.isValueAddedTaxEnabled = function() {
410   return this.valueAddedTaxEnabled;
411 }
412 
413 /**
414  * Sets whether Value Added Tax should be taken in account in prices.
415  * @param valueAddedTaxEnabled if <code>true</code> VAT will be added to prices.
416  * @since 6.0
417  * @ignore
418  */
419 UserPreferences.prototype.setValueAddedTaxEnabled = function(valueAddedTaxEnabled) {
420   if (this.valueAddedTaxEnabled !== valueAddedTaxEnabled) {
421     this.valueAddedTaxEnabled = valueAddedTaxEnabled;
422     this.propertyChangeSupport.firePropertyChange("VALUE_ADDED_TAX_ENABLED",
423         !valueAddedTaxEnabled, valueAddedTaxEnabled);
424   }
425 }
426 
427 /**
428  * Returns the Value Added Tax percentage applied to prices by default, or <code>null</code>
429  * if VAT isn't taken into account in the application.
430  * @since 6.0
431  * @ignore
432  */
433 UserPreferences.prototype.getDefaultValueAddedTaxPercentage = function() {
434   return this.defaultValueAddedTaxPercentage;
435 }
436 
437 /**
438  * Sets the Value Added Tax percentage applied to prices by default.
439  * @param {Big} valueAddedTaxPercentage the default VAT percentage
440  * @since 6.0
441  * @ignore
442  */
443 UserPreferences.prototype.setDefaultValueAddedTaxPercentage = function(valueAddedTaxPercentage) {
444   if (valueAddedTaxPercentage !== this.defaultValueAddedTaxPercentage
445       && (valueAddedTaxPercentage == null || this.defaultValueAddedTaxPercentage == null || !valueAddedTaxPercentage.eq(this.defaultValueAddedTaxPercentage))) {
446     var oldValueAddedTaxPercentage = this.defaultValueAddedTaxPercentage;
447     this.defaultValueAddedTaxPercentage = valueAddedTaxPercentage;
448     this.propertyChangeSupport.firePropertyChange("DEFAULT_VALUE_ADDED_TAX_PERCENTAGE", oldValueAddedTaxPercentage, valueAddedTaxPercentage);
449 
450   }
451 }
452 
453 /**
454  * Returns <code>true</code> if the furniture catalog should be viewed in a tree.
455  * @return {boolean}
456  * @ignore
457  */
458 UserPreferences.prototype.isFurnitureCatalogViewedInTree = function() {
459   return this.furnitureCatalogViewedInTree;
460 }
461 
462 /**
463  * Sets whether the furniture catalog should be viewed in a tree or a different way.
464  * @param {boolean}
465  * @ignore
466  */
467 UserPreferences.prototype.setFurnitureCatalogViewedInTree = function(furnitureCatalogViewedInTree) {
468   if (this.furnitureCatalogViewedInTree !== furnitureCatalogViewedInTree) {
469     this.furnitureCatalogViewedInTree = furnitureCatalogViewedInTree;
470     this.propertyChangeSupport.firePropertyChange("FURNITURE_CATALOG_VIEWED_IN_TREE", 
471         !furnitureCatalogViewedInTree, furnitureCatalogViewedInTree);
472   }
473 }
474 
475 /**
476  * Returns <code>true</code> if the navigation panel should be displayed.
477  * @return {boolean}
478  */
479 UserPreferences.prototype.isNavigationPanelVisible = function() {
480   return this.navigationPanelVisible;
481 }
482 
483 /**
484  * Sets whether the navigation panel should be displayed or not.
485  * @param {boolean} navigationPanelVisible
486  */
487 UserPreferences.prototype.setNavigationPanelVisible = function(navigationPanelVisible) {
488   if (this.navigationPanelVisible !== navigationPanelVisible) {
489     this.navigationPanelVisible = navigationPanelVisible;
490     this.propertyChangeSupport.firePropertyChange("NAVIGATION_PANEL_VISIBLE", 
491         !navigationPanelVisible, navigationPanelVisible);
492   }
493 }
494 
495 /**
496  * Returns whether interactive editing in 3D view is enabled or not.
497  * @return {boolean}
498  * @since 7.2
499  */
500 UserPreferences.prototype.isEditingIn3DViewEnabled = function() {
501   return this.editingIn3DViewEnabled;
502 }
503 
504 /**
505  * Sets whether interactive editing in 3D view is enabled or not.
506  * @param {boolean} editingIn3DViewEnabled
507  * @since 7.2
508  */
509 UserPreferences.prototype.setEditingIn3DViewEnabled = function(editingIn3DViewEnabled) {
510   if (editingIn3DViewEnabled != this.editingIn3DViewEnabled) {
511     this.editingIn3DViewEnabled = editingIn3DViewEnabled;
512     this.propertyChangeSupport.firePropertyChange("EDITING_IN_3D_VIEW_ENABLED",
513         !editingIn3DViewEnabled, editingIn3DViewEnabled);
514   }
515 }
516 
517 /**
518  * Returns whether observer camera should be centered on selection or not.
519  * @return {boolean}
520  */
521 UserPreferences.prototype.isAerialViewCenteredOnSelectionEnabled = function() {
522   return this.aerialViewCenteredOnSelectionEnabled;
523 }
524 
525 /**
526  * Sets whether aerial view should be centered on selection or not.
527  * @param {boolean} aerialViewCenteredOnSelectionEnabled
528  */
529 UserPreferences.prototype.setAerialViewCenteredOnSelectionEnabled = function(aerialViewCenteredOnSelectionEnabled) {
530   if (aerialViewCenteredOnSelectionEnabled !== this.aerialViewCenteredOnSelectionEnabled) {
531     this.aerialViewCenteredOnSelectionEnabled = aerialViewCenteredOnSelectionEnabled;
532     this.propertyChangeSupport.firePropertyChange("AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED", 
533         !aerialViewCenteredOnSelectionEnabled, aerialViewCenteredOnSelectionEnabled);
534   }
535 }
536 
537 /**
538  * Returns whether the observer camera should be selected at each change.
539  * @return {boolean}
540  * @since 5.5
541  */
542 UserPreferences.prototype.isObserverCameraSelectedAtChange = function() {
543   return this.observerCameraSelectedAtChange;
544 }
545 
546 /**
547  * Sets whether the observer camera should be selected at each change.
548  * @param {boolean} observerCameraSelectedAtChange
549  * @since 5.5
550  */
551 UserPreferences.prototype.setObserverCameraSelectedAtChange = function(observerCameraSelectedAtChange) {
552   if (observerCameraSelectedAtChange !== this.observerCameraSelectedAtChange) {
553     this.observerCameraSelectedAtChange = observerCameraSelectedAtChange;
554     this.propertyChangeSupport.firePropertyChange("OBSERVER_CAMERA_SELECTED_AT_CHANGE", 
555         !observerCameraSelectedAtChange, observerCameraSelectedAtChange);
556   }
557 }
558 
559 /**
560  * Returns <code>true</code> if magnetism is enabled.
561  * @return {boolean} <code>true</code> by default.
562  */
563 UserPreferences.prototype.isMagnetismEnabled = function() {
564   return this.magnetismEnabled;
565 }
566 
567 /**
568  * Sets whether magnetism is enabled or not, and notifies
569  * listeners of this change. 
570  * @param {boolean} magnetismEnabled <code>true</code> if magnetism is enabled,
571  *          <code>false</code> otherwise.
572  */
573 UserPreferences.prototype.setMagnetismEnabled = function(magnetismEnabled) {
574   if (this.magnetismEnabled !== magnetismEnabled) {
575     this.magnetismEnabled = magnetismEnabled;
576     this.propertyChangeSupport.firePropertyChange("MAGNETISM_ENABLED", 
577         !magnetismEnabled, magnetismEnabled);
578   }
579 }
580 
581 /**
582  * Returns <code>true</code> if rulers are visible.
583  * @return {boolean} <code>true</code> by default.
584  * @ignore
585  */
586 UserPreferences.prototype.isRulersVisible = function() {
587   return this.rulersVisible;
588 }
589 
590 /**
591  * Sets whether rulers are visible or not, and notifies
592  * listeners of this change. 
593  * @param {boolean} rulersVisible <code>true</code> if rulers are visible,
594  *          <code>false</code> otherwise.
595  * @ignore
596  */
597 UserPreferences.prototype.setRulersVisible = function(rulersVisible) {
598   if (this.rulersVisible !== rulersVisible) {
599     this.rulersVisible = rulersVisible;
600     this.propertyChangeSupport.firePropertyChange("RULERS_VISIBLE", 
601         !rulersVisible, rulersVisible);
602   }
603 }
604 
605 /**
606  * Returns <code>true</code> if plan grid visible.
607  * @return {boolean} <code>true</code> by default.
608  */
609 UserPreferences.prototype.isGridVisible = function() {
610   return this.gridVisible;
611 }
612 
613 /**
614  * Sets whether plan grid is visible or not, and notifies
615  * listeners of this change. 
616  * @param {boolean} gridVisible <code>true</code> if grid is visible,
617  *          <code>false</code> otherwise.
618  */
619 UserPreferences.prototype.setGridVisible = function(gridVisible) {
620   if (this.gridVisible !== gridVisible) {
621     this.gridVisible = gridVisible;
622     this.propertyChangeSupport.firePropertyChange("GRID_VISIBLE", 
623         !gridVisible, gridVisible);
624   }
625 }
626 
627 /**
628  * Returns the name of the font that should be used by default or <code>null</code> 
629  * if the default font should be the default one in the application.
630  * @return {string}
631  */
632 UserPreferences.prototype.getDefaultFontName = function() {
633   return this.defaultFontName;
634 }
635 
636 /**
637  * Sets the name of the font that should be used by default.
638  * @param {string} defaultFontName
639  */
640 UserPreferences.prototype.setDefaultFontName = function(defaultFontName) {
641   if (defaultFontName != this.defaultFontName) {
642     var oldName = this.defaultFontName;
643     this.defaultFontName = defaultFontName;
644     this.propertyChangeSupport.firePropertyChange("DEFAULT_FONT_NAME", oldName, defaultFontName);
645   }
646 }
647 
648 /**
649  * Returns <code>true</code> if furniture should be viewed from its top in plan.
650  * @return {boolean}
651  */
652 UserPreferences.prototype.isFurnitureViewedFromTop = function() {
653   return this.furnitureViewedFromTop;
654 }
655 
656 /**
657  * Sets how furniture icon should be displayed in plan, and notifies
658  * listeners of this change. 
659  * @param {boolean} furnitureViewedFromTop if <code>true</code> the furniture 
660  *    should be viewed from its top.
661  */
662 UserPreferences.prototype.setFurnitureViewedFromTop = function(furnitureViewedFromTop) {
663   if (this.furnitureViewedFromTop !== furnitureViewedFromTop) {
664     this.furnitureViewedFromTop = furnitureViewedFromTop;
665     this.propertyChangeSupport.firePropertyChange("FURNITURE_VIEWED_FROM_TOP", 
666         !furnitureViewedFromTop, furnitureViewedFromTop);
667   }
668 }
669 
670 /**
671  * Returns the size used to generate icons of furniture viewed from top.
672  * @since 5.5
673  */
674 UserPreferences.prototype.getFurnitureModelIconSize = function() {
675   return this.furnitureModelIconSize;
676 }
677 
678 /**
679  * Sets the name of the font that should be used by default.
680  * @since 5.5
681  */
682 UserPreferences.prototype.setFurnitureModelIconSize = function(furnitureModelIconSize) {
683   if (furnitureModelIconSize !== this.furnitureModelIconSize) {
684     var oldSize = this.furnitureModelIconSize;
685     this.furnitureModelIconSize = furnitureModelIconSize;
686     this.propertyChangeSupport.firePropertyChange("FURNITURE_MODEL_ICON_SIZE", oldSize, furnitureModelIconSize);
687   }
688 }
689 
690 /**
691  * Returns <code>true</code> if room floors should be rendered with color or texture in plan.
692  * @return <code>false</code> by default.
693  */
694 UserPreferences.prototype.isRoomFloorColoredOrTextured = function() {
695   return this.roomFloorColoredOrTextured;
696 }
697 
698 /**
699  * Sets whether room floors should be rendered with color or texture, 
700  * and notifies listeners of this change. 
701  * @param roomFloorColoredOrTextured <code>true</code> if floor color 
702  *          or texture is used, <code>false</code> otherwise.
703  */
704 UserPreferences.prototype.setFloorColoredOrTextured = function(roomFloorColoredOrTextured) {
705   if (this.roomFloorColoredOrTextured !== roomFloorColoredOrTextured) {
706     this.roomFloorColoredOrTextured = roomFloorColoredOrTextured;
707     this.propertyChangeSupport.firePropertyChange("ROOM_FLOOR_COLORED_OR_TEXTURED", 
708         !roomFloorColoredOrTextured, roomFloorColoredOrTextured);
709   }
710 }
711 
712 /**
713  * Returns the wall pattern in plan used by default.
714  * @return {TextureImage}
715  * @ignore
716  */
717 UserPreferences.prototype.getWallPattern = function() {
718   return this.wallPattern;
719 }
720 
721 /**
722  * Sets how walls should be displayed in plan by default, and notifies
723  * listeners of this change.
724  * @ignore
725  */
726 UserPreferences.prototype.setWallPattern = function(wallPattern) {
727   if (this.wallPattern !== wallPattern) {
728     var oldWallPattern = this.wallPattern;
729     this.wallPattern = wallPattern;
730     this.propertyChangeSupport.firePropertyChange("WALL_PATTERN", 
731         oldWallPattern, wallPattern);
732   }
733 }
734 
735 /**
736  * Returns the pattern used for new walls in plan or <code>null</code> if it's not set.
737  * @return {TextureImage}
738  */
739 UserPreferences.prototype.getNewWallPattern = function() {
740   return this.newWallPattern;
741 }
742 
743 /**
744  * Sets how new walls should be displayed in plan, and notifies
745  * listeners of this change.
746  */
747 UserPreferences.prototype.setNewWallPattern = function(newWallPattern) {
748   if (this.newWallPattern !== newWallPattern) {
749     var oldWallPattern = this.newWallPattern;
750     this.newWallPattern = newWallPattern;
751     this.propertyChangeSupport.firePropertyChange("NEW_WALL_PATTERN", 
752         oldWallPattern, newWallPattern);
753   }
754 }
755 
756 /**
757  * Returns default thickness of new walls in home. 
758  */
759 UserPreferences.prototype.getNewWallThickness = function() {
760   return this.newWallThickness;
761 }
762 
763 /**
764  * Sets default thickness of new walls in home, and notifies
765  * listeners of this change.  
766  */
767 UserPreferences.prototype.setNewWallThickness = function(newWallThickness) {
768   if (this.newWallThickness !== newWallThickness) {
769     var oldDefaultThickness = this.newWallThickness;
770     this.newWallThickness = newWallThickness;
771     this.propertyChangeSupport.firePropertyChange("NEW_WALL_THICKNESS", 
772         oldDefaultThickness, newWallThickness);
773   }
774 }
775 
776 /**
777  * Returns default wall height of new home walls. 
778  */
779 UserPreferences.prototype.getNewWallHeight = function() {
780   return this.newWallHeight;
781 }
782 
783 /**
784  * Sets default wall height of new walls, and notifies
785  * listeners of this change. 
786  */
787 UserPreferences.prototype.setNewWallHeight = function(newWallHeight) {
788   if (this.newWallHeight !== newWallHeight) {
789     var oldWallHeight = this.newWallHeight;
790     this.newWallHeight = newWallHeight;
791     this.propertyChangeSupport.firePropertyChange("NEW_WALL_HEIGHT", 
792         oldWallHeight, newWallHeight);
793   }
794 }
795 
796 /**
797  * Returns default baseboard thickness of new walls in home. 
798  */
799 UserPreferences.prototype.getNewWallBaseboardThickness = function() {
800   return this.newWallBaseboardThickness;
801 }
802 
803 /**
804  * Sets default baseboard thickness of new walls in home, and notifies
805  * listeners of this change.  
806  */
807 UserPreferences.prototype.setNewWallBaseboardThickness = function(newWallBaseboardThickness) {
808   if (this.newWallBaseboardThickness !== newWallBaseboardThickness) {
809     var oldThickness = this.newWallBaseboardThickness;
810     this.newWallBaseboardThickness = newWallBaseboardThickness;
811     this.propertyChangeSupport.firePropertyChange("NEW_WALL_SIDEBOARD_THICKNESS", 
812         oldThickness, newWallBaseboardThickness);
813   }
814 }
815 
816 /**
817  * Returns default baseboard height of new home walls. 
818  */
819 UserPreferences.prototype.getNewWallBaseboardHeight = function() {
820   return this.newWallBaseboardHeight;
821 }
822 
823 /**
824  * Sets default baseboard height of new walls, and notifies
825  * listeners of this change. 
826  */
827 UserPreferences.prototype.setNewWallBaseboardHeight = function(newWallBaseboardHeight) {
828   if (this.newWallBaseboardHeight !== newWallBaseboardHeight) {
829     var oldHeight = this.newWallBaseboardHeight;
830     this.newWallBaseboardHeight = newWallBaseboardHeight;
831     this.propertyChangeSupport.firePropertyChange("NEW_WALL_SIDEBOARD_HEIGHT", 
832         oldHeight, newWallBaseboardHeight);
833   }
834 }
835 
836 /**
837  * Returns the default color of new rooms in home.
838  * @since 6.4
839  */
840 UserPreferences.prototype.getNewRoomFloorColor = function() {
841   return this.newRoomFloorColor;
842 }
843 
844 /**
845  * Sets the default color of new rooms in home, and notifies
846  * listeners of this change.
847  * @since 6.4
848  */
849 UserPreferences.prototype.setNewRoomFloorColor = function(newRoomFloorColor) {
850   if (this.newRoomFloorColor !== newRoomFloorColor) {
851     var oldRoomFloorColor = this.newRoomFloorColor;
852     this.newRoomFloorColor = newRoomFloorColor;
853     this.propertyChangeSupport.firePropertyChange("NEW_ROOM_FLOOR_COLOR",
854         oldRoomFloorColor, newRoomFloorColor);
855   }
856 }
857 
858 /**
859  * Returns default thickness of the floor of new levels in home. 
860  */
861 UserPreferences.prototype.getNewFloorThickness = function() {
862   return this.newFloorThickness;
863 }
864 
865 /**
866  * Sets default thickness of the floor of new levels in home, and notifies
867  * listeners of this change.  
868  */
869 UserPreferences.prototype.setNewFloorThickness = function(newFloorThickness) {
870   if (this.newFloorThickness !== newFloorThickness) {
871     var oldFloorThickness = this.newFloorThickness;
872     this.newFloorThickness = newFloorThickness;
873     this.propertyChangeSupport.firePropertyChange("NEW_FLOOR_THICKNESS", 
874         oldFloorThickness, newFloorThickness);
875   }
876 }
877 
878 /**
879  * Returns the delay between two automatic save operations of homes for recovery purpose.
880  * @return a delay in milliseconds or 0 to disable auto save.
881  * @ignore
882  */
883 UserPreferences.prototype.getAutoSaveDelayForRecovery = function() {
884   return this.autoSaveDelayForRecovery;
885 }
886 
887 /**
888  * Sets the delay between two automatic save operations of homes for recovery purpose.
889  * @ignore
890  */
891 UserPreferences.prototype.setAutoSaveDelayForRecovery = function(autoSaveDelayForRecovery) {
892   if (this.autoSaveDelayForRecovery !== autoSaveDelayForRecovery) {
893     var oldAutoSaveDelayForRecovery = this.autoSaveDelayForRecovery;
894     this.autoSaveDelayForRecovery = autoSaveDelayForRecovery;
895     this.propertyChangeSupport.firePropertyChange("AUTO_SAVE_DELAY_FOR_RECOVERY", 
896         oldAutoSaveDelayForRecovery, autoSaveDelayForRecovery);
897   }
898 }
899 
900 /**
901  * Returns an unmodifiable list of the recent homes.
902  * @ignore
903  */
904 UserPreferences.prototype.getRecentHomes = function() {
905   return this.recentHomes.slice(0);
906 }
907 
908 /**
909  * Sets the recent homes list and notifies listeners of this change.
910  * @ignore
911  */
912 UserPreferences.prototype.setRecentHomes = function(recentHomes) {
913   if (recentHomes != this.recentHomes) {
914     var oldRecentHomes = this.recentHomes;
915     this.recentHomes = recentHomes.slice(0);
916     this.propertyChangeSupport.firePropertyChange("RECENT_HOMES", 
917         oldRecentHomes, this.getRecentHomes());
918   }
919 }
920 
921 /**
922  * Returns the maximum count of homes that should be proposed to the user.
923  * @ignore
924  */
925 UserPreferences.prototype.getRecentHomesMaxCount = function() {
926   return 10;
927 }
928 
929 /**
930  * Returns the maximum count of stored cameras in homes that should be proposed to the user.
931  * @ignore
932  */
933 UserPreferences.prototype.getStoredCamerasMaxCount = function() {
934   return 50;
935 }
936 
937 /**
938  * Sets which action tip should be ignored.
939  * <br>This method should be overridden to store the ignore information.
940  * By default it just notifies listeners of this change. 
941  * @ignore
942  */
943 UserPreferences.prototype.setActionTipIgnored = function(actionKey) {    
944   this.propertyChangeSupport.firePropertyChange("IGNORED_ACTION_TIP", null, actionKey);
945 }
946 
947 /**
948  * Returns whether an action tip should be ignored or not. 
949  * <br>This method should be overridden to return the display information
950  * stored in setActionTipIgnored.
951  * By default it returns <code>true</code>. 
952  * @ignore
953  */
954 UserPreferences.prototype.isActionTipIgnored = function(actionKey) {
955   return true;
956 }
957 
958 /**
959  * Resets the ignore flag of action tips.
960  * <br>This method should be overridden to clear all the display flags.
961  * By default it just notifies listeners of this change. 
962  * @ignore
963  */
964 UserPreferences.prototype.resetIgnoredActionTips = function() {    
965   this.propertyChangeSupport.firePropertyChange("IGNORED_ACTION_TIP", null, null);
966 }
967 
968 /**
969  * Returns the default text style of a class of selectable item. 
970  * @ignore
971  */
972 UserPreferences.prototype.getDefaultTextStyle = function(selectableClass) {
973   if (selectableClass.name == "Room") {
974     return UserPreferences.DEFAULT_ROOM_TEXT_STYLE;
975   } else {
976     return UserPreferences.DEFAULT_TEXT_STYLE;
977   }
978 }
979 
980 /**
981  * Returns the strings that may be used for the auto completion of the given <code>property</code>.
982  * @ignore
983  */
984 UserPreferences.prototype.getAutoCompletionStrings = function(property) {
985   var propertyAutoCompletionStrings = this.autoCompletionStrings.get(property);
986   if (propertyAutoCompletionStrings !== undefined) {
987     return propertyAutoCompletionStrings.slice(0);
988   } else {
989     return [];
990   }
991 }
992 
993 /**
994  * Adds the given string to the list of the strings used in auto completion of a <code>property</code>
995  * and notifies listeners of this change.
996  * @ignore
997  */
998 UserPreferences.prototype.addAutoCompletionString = function(property, autoCompletionString) {
999   if (autoCompletionString !== null 
1000       && autoCompletionString.length > 0) {
1001     var propertyAutoCompletionStrings = this.autoCompletionStrings [property];
1002     if (propertyAutoCompletionStrings === undefined) {
1003       propertyAutoCompletionStrings = [];
1004     } else if (propertyAutoCompletionStrings.indexOf(autoCompletionString) < 0) {
1005       propertyAutoCompletionStrings = propertyAutoCompletionStrings.slice(0);
1006     } else {
1007       return;
1008     }
1009     propertyAutoCompletionStrings.splice(0, 0, autoCompletionString);
1010     this.setAutoCompletionStrings(property, propertyAutoCompletionStrings);
1011   }
1012 }
1013 
1014 /**
1015  * Sets the auto completion strings list of the given <code>property</code> and notifies listeners of this change.
1016  * @ignore
1017  */
1018 UserPreferences.prototype.setAutoCompletionStrings = function(property, autoCompletionStrings) {
1019   var propertyAutoCompletionStrings = this.autoCompletionStrings [property];
1020   if (autoCompletionStrings != propertyAutoCompletionStrings) {
1021     this.autoCompletionStrings [property] = autoCompletionStrings.slice(0);
1022     this.propertyChangeSupport.firePropertyChange("AUTO_COMPLETION_STRINGS", 
1023         null, property);
1024   }
1025 }
1026 
1027 /**
1028  * Returns the list of properties with auto completion strings. 
1029  * @ignore
1030  */
1031 UserPreferences.prototype.getAutoCompletedProperties = function() {
1032   if (this.autoCompletionStrings !== null) {
1033     return Object.keys(this.autoCompletionStrings);
1034   } else {
1035     return [];
1036   }
1037 }
1038 
1039 /**
1040  * Returns the list of the recent colors.
1041  * @ignore
1042  */
1043 UserPreferences.prototype.getRecentColors = function() {
1044   return this.recentColors;
1045 }
1046 
1047 /**
1048  * Sets the recent colors list and notifies listeners of this change.
1049  * @ignore
1050  */
1051 UserPreferences.prototype.setRecentColors = function(recentColors) {
1052   if (recentColors != this.recentColors) {
1053     var oldRecentColors = this.recentColors;
1054     this.recentColors = recentColors.slice(0);
1055     this.propertyChangeSupport.firePropertyChange("RECENT_COLORS", 
1056         oldRecentColors, this.getRecentColors());
1057   }
1058 }
1059 
1060 /**
1061  * Returns the list of the recent textures.
1062  * @ignore
1063  */
1064 UserPreferences.prototype.getRecentTextures = function() {
1065   return this.recentTextures;
1066 }
1067 
1068 /**
1069  * Sets the recent colors list and notifies listeners of this change.
1070  * @ignore
1071  */
1072 UserPreferences.prototype.setRecentTextures = function(recentTextures) {
1073   if (recentTextures != this.recentTextures) {
1074     var oldRecentTextures = this.recentTextures;
1075     this.recentTextures = recentTextures.slice(0);
1076     this.propertyChangeSupport.firePropertyChange("RECENT_TEXTURES", 
1077         oldRecentTextures, this.getRecentTextures());
1078   }
1079 }
1080 
1081 /**
1082  * Sets the home examples available for the user.
1083  * @param {HomeDescriptor[]} homeExamples an array of examples
1084  * @since 5.5
1085  * @ignore
1086  */
1087 UserPreferences.prototype.setHomeExamples = function(homeExamples) {
1088   if (homeExamples != this.homeExamples) {
1089     var oldExamples = this.homeExamples;
1090     this.homeExamples = homeExamples.slice(0);
1091     this.propertyChangeSupport.firePropertyChange("HOME_EXAMPLES", 
1092         oldExamples, this.getHomeExamples());
1093   }
1094 }
1095 
1096 /**
1097  * Returns the home examples available for the user.
1098  * @return {HomeDescriptor[]} an array of examples.
1099  * @since 5.5
1100  * @ignore
1101  */
1102 UserPreferences.prototype.getHomeExamples = function() {
1103   return this.homeExamples;
1104 }
1105 
1106 /**
1107  * @return {boolean} <code>true</code> if updates should be checked.
1108  * @ignore
1109  */
1110 UserPreferences.prototype.isCheckUpdatesEnabled = function() {
1111   // Empty implementation because it is used by the controller but useless for the Web version
1112 }
1113 
1114 /**
1115  * Sets whether updates should be checked or not.
1116  * @param {boolean} updatesChecked 
1117  * @since 4.0
1118  */
1119 UserPreferences.prototype.setCheckUpdatesEnabled = function(updatesChecked) {
1120   // Empty implementation because it is used by the controller but useless for the Web version
1121 }
1122 
1123 /**
1124  * Returns <code>true</code> if large imported images should be resized without requesting user.
1125  * @ignore
1126  */
1127 UserPreferences.prototype.isImportedImageResizedWithoutPrompting = function() {
1128   return true;
1129 }
1130 
1131 
1132 /**
1133  * Default user preferences.
1134  * @param {string[]|boolean} [furnitureCatalogUrls]
1135  * @param {string}   [furnitureResourcesUrlBase]
1136  * @param {string[]} [texturesCatalogUrls]
1137  * @param {string}   [texturesResourcesUrlBase]
1138  * @constructor
1139  * @extends UserPreferences
1140  * @author Emmanuel Puybaret
1141  */
1142 function DefaultUserPreferences(furnitureCatalogUrls, furnitureResourcesUrlBase, 
1143                                 texturesCatalogUrls, texturesResourcesUrlBase) {
1144   UserPreferences.call(this);
1145 
1146   var readCatalogs;
1147   var userLanguage;
1148   if (furnitureCatalogUrls !== undefined 
1149       && (typeof furnitureCatalogUrls === "boolean")) {
1150     readCatalogs = furnitureCatalogUrls;
1151     userLanguage = furnitureResourcesUrlBase;
1152     this.furnitureCatalogUrls = undefined;
1153     this.furnitureResourcesUrlBase = undefined;
1154     this.texturesCatalogUrls = undefined;
1155     this.texturesResourcesUrlBase = undefined;
1156   } else {
1157     readCatalogs = true;
1158     userLanguage = Locale.getDefault();
1159     this.furnitureCatalogUrls = furnitureCatalogUrls;
1160     this.furnitureResourcesUrlBase = furnitureResourcesUrlBase;
1161     this.texturesCatalogUrls = texturesCatalogUrls;
1162     this.texturesResourcesUrlBase = texturesResourcesUrlBase;
1163   }
1164   
1165   // Build default patterns catalog
1166   var patterns = [];
1167   patterns.push(new DefaultPatternTexture("foreground"));
1168   patterns.push(new DefaultPatternTexture("reversedHatchUp"));
1169   patterns.push(new DefaultPatternTexture("reversedHatchDown"));
1170   patterns.push(new DefaultPatternTexture("reversedCrossHatch"));
1171   patterns.push(new DefaultPatternTexture("background"));
1172   patterns.push(new DefaultPatternTexture("hatchUp"));
1173   patterns.push(new DefaultPatternTexture("hatchDown"));
1174   patterns.push(new DefaultPatternTexture("crossHatch"));
1175   var patternsCatalog = new PatternsCatalog(patterns);  
1176   this.setPatternsCatalog(patternsCatalog);
1177   this.setFurnitureCatalog(readCatalogs && (typeof DefaultFurnitureCatalog === "function")
1178       ? (Array.isArray(furnitureCatalogUrls)
1179            ? new DefaultFurnitureCatalog(furnitureCatalogUrls, furnitureResourcesUrlBase) 
1180            : new DefaultFurnitureCatalog(this))
1181       : new FurnitureCatalog());
1182   this.setTexturesCatalog(readCatalogs && (typeof DefaultTexturesCatalog === "function")
1183       ? (Array.isArray(texturesCatalogUrls)
1184            ? new DefaultTexturesCatalog(texturesCatalogUrls, texturesResourcesUrlBase) 
1185            : new DefaultTexturesCatalog(this))
1186       : new TexturesCatalog());
1187 
1188   if (userLanguage == "en_US") {
1189     this.setUnit(LengthUnit.INCH);
1190     this.setNewWallThickness(7.62);
1191     this.setNewWallHeight(243.84);
1192     this.setNewWallBaseboardThickness(0.9525);
1193     this.setNewWallBaseboardHeight(6.35);
1194   } else {
1195     this.setUnit(LengthUnit.CENTIMETER);
1196     this.setNewWallThickness(7.5);
1197     this.setNewWallHeight(250);
1198     this.setNewWallBaseboardThickness(1);
1199     this.setNewWallBaseboardHeight(7);
1200   }
1201 
1202   this.setNewFloorThickness(12);
1203   this.setNavigationPanelVisible(false);
1204   this.setWallPattern(patternsCatalog.getPattern("hatchUp"));
1205   this.setNewWallPattern(this.getWallPattern());
1206   this.setAerialViewCenteredOnSelectionEnabled(true);
1207   this.setAutoSaveDelayForRecovery(60000);
1208 }
1209 DefaultUserPreferences.prototype = Object.create(UserPreferences.prototype);
1210 DefaultUserPreferences.prototype.constructor = DefaultUserPreferences;
1211 
1212 /**
1213  * Writes user preferences.
1214  */
1215 DefaultUserPreferences.prototype.write = function() {
1216   UserPreferences.prototype.write.call(this);
1217 }
1218 
1219 
1220 /**
1221  * Creates a pattern built from resources.
1222  * @param {string} name
1223  * @constructor
1224  * @ignore
1225  * @author Emmanuel Puybaret
1226  */
1227 function DefaultPatternTexture(name) {
1228   this.name = name;
1229   this.image = new URLContent(ZIPTools.getScriptFolder() + "resources/patterns/" + this.name + ".png");
1230 }
1231 
1232 DefaultPatternTexture["__class"] = "com.eteks.sweethome3d.io.DefaultPatternTexture";
1233 DefaultPatternTexture["__interfaces"] = ["com.eteks.sweethome3d.model.TextureImage"];
1234 DefaultPatternTexture['__transients'] = ["image"];
1235 
1236 /**
1237  * Returns the name of this texture.
1238  * @return {string}
1239  */
1240 DefaultPatternTexture.prototype.getName = function () {
1241   return this.name;
1242 }
1243 
1244 /**
1245  * Returns the creator of this texture.
1246  * @return {string}
1247  */
1248 DefaultPatternTexture.prototype.getCreator = function () {
1249   return null;
1250 }
1251 
1252 /**
1253  * Returns the content of the image used for this texture.
1254  * @return {Object}
1255  */
1256 DefaultPatternTexture.prototype.getImage = function () {
1257   return this.image;
1258 }
1259 
1260 /**
1261  * Returns the width of the image in centimeters.
1262  * @return {number}
1263  */
1264 DefaultPatternTexture.prototype.getWidth = function () {
1265   return 10;
1266 }
1267 
1268 /**
1269  * Returns the height of the image in centimeters.
1270  * @return {number}
1271  */
1272 DefaultPatternTexture.prototype.getHeight = function () {
1273   return 10;
1274 }
1275 
1276 /**
1277  * Returns <code>true</code> if the object in parameter is equal to this texture.
1278  * @param {Object} obj
1279  * @return {boolean}
1280  */
1281 DefaultPatternTexture.prototype.equals = function (obj) {
1282   if (obj === this) {
1283     return true;
1284   } else if (obj instanceof DefaultPatternTexture) {
1285     var pattern = obj;
1286     return pattern.name == this.name;
1287   } else {
1288     return false;
1289   }
1290 }
1291 
1292 
1293 /**
1294  * User's preferences, synchronized with a backend.
1295  * @param {{furnitureCatalogURLs: string[],
1296  *          furnitureResourcesURLBase: string,
1297  *          texturesCatalogURLs: string[],
1298  *          texturesResourcesURLBase: string,
1299  *          writePreferencesURL: string,
1300  *          readPreferencesURL: string,
1301  *          writeResourceURL: string,
1302  *          readResourceURL: string,
1303  *          writePreferencesResourceURL: string,
1304  *          readPreferencesResourceURL: string,
1305  *          defaultUserLanguage: string,
1306  *          writingObserver: {writeStarted: Function, 
1307  *                            writeSucceeded: Function, 
1308  *                            writeFailed: Function, 
1309  *                            connectionFound: Function, 
1310  *                            connectionLost: Function}
1311  *         }} [configuration] preferences configuration.
1312  *              If configuration.writePreferencesResourceURL / configuration.readPreferencesResourceURL is missing,
1313  *              configuration.writeResourceURL / configuration.readResourceURL will be used.
1314  * @constructor
1315  * @extends UserPreferences
1316  * @author Louis Grignon
1317  * @author Emmanuel Puybaret
1318  */
1319 function RecordedUserPreferences(configuration) {
1320   UserPreferences.call(this);
1321 
1322   if (configuration !== undefined) {
1323     this.furnitureCatalogUrls = configuration.furnitureCatalogURLs;
1324     this.furnitureResourcesUrlBase = configuration.furnitureResourcesURLBase;
1325     this.texturesCatalogUrls = configuration.texturesCatalogURLs;
1326     this.texturesResourcesUrlBase = configuration.texturesResourcesURLBase;
1327     this.writePreferencesUrl = configuration.writePreferencesURL;
1328     this.readPreferencesUrl = configuration.readPreferencesURL;
1329     this.writeResourceUrl = configuration.writePreferencesResourceURL !== undefined 
1330         ? configuration.writePreferencesResourceURL : configuration.writeResourceURL;
1331     this.readResourceUrl = configuration.readPreferencesResourceURL !== undefined 
1332         ? configuration.readPreferencesResourceURL : configuration.readResourceURL;
1333     this.writingObserver = configuration.writingObserver;
1334   }
1335   
1336   var userLanguage;
1337   if (configuration !== undefined && configuration.defaultUserLanguage !== undefined) {
1338     userLanguage = configuration.defaultUserLanguage;
1339   } else { 
1340     userLanguage = this.getLanguage();
1341   }
1342 
1343   this.uploadingBlobs = {};
1344   this.properties = {};
1345   this.setFurnitureCatalog(new FurnitureCatalog());
1346   this.setTexturesCatalog(new TexturesCatalog());
1347   if (this.readPreferencesUrl) {
1348     this.updatePreferencesFromProperties(this.properties, userLanguage, false);
1349     this.readPreferences(this.properties, userLanguage);
1350   } else {
1351     // Initialize properties from default preferences
1352     this.updatePreferencesFromProperties(this.properties, userLanguage, true);
1353     this.addListeners();
1354   }
1355 
1356 }
1357 RecordedUserPreferences.prototype = Object.create(UserPreferences.prototype);
1358 RecordedUserPreferences.prototype.constructor = RecordedUserPreferences;
1359 
1360 RecordedUserPreferences.LANGUAGE                                  = "language";
1361 RecordedUserPreferences.UNIT                                      = "unit";
1362 RecordedUserPreferences.CURRENCY                                  = "currency";
1363 RecordedUserPreferences.VALUE_ADDED_TAX_ENABLED                   = "valueAddedTaxEnabled";
1364 RecordedUserPreferences.DEFAULT_VALUE_ADDED_TAX_PERCENTAGE        = "defaultValueAddedTaxPercentage";
1365 RecordedUserPreferences.FURNITURE_CATALOG_VIEWED_IN_TREE          = "furnitureCatalogViewedInTree";
1366 RecordedUserPreferences.NAVIGATION_PANEL_VISIBLE                  = "navigationPanelVisible";
1367 RecordedUserPreferences.EDITING_IN_3D_VIEW_ENABLED                = "editingIn3DViewEnabled";
1368 RecordedUserPreferences.AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED = "aerialViewCenteredOnSelectionEnabled";
1369 RecordedUserPreferences.OBSERVER_CAMERA_SELECTED_AT_CHANGE        = "observerCameraSelectedAtChange";
1370 RecordedUserPreferences.MAGNETISM_ENABLED                         = "magnetismEnabled";
1371 RecordedUserPreferences.RULERS_VISIBLE                            = "rulersVisible";
1372 RecordedUserPreferences.GRID_VISIBLE                              = "gridVisible";
1373 RecordedUserPreferences.DEFAULT_FONT_NAME                         = "defaultFontName";
1374 RecordedUserPreferences.FURNITURE_VIEWED_FROM_TOP                 = "furnitureViewedFromTop";
1375 RecordedUserPreferences.FURNITURE_MODEL_ICON_SIZE                 = "furnitureModelIconSize";
1376 RecordedUserPreferences.ROOM_FLOOR_COLORED_OR_TEXTURED            = "roomFloorColoredOrTextured";
1377 RecordedUserPreferences.WALL_PATTERN                              = "wallPattern";
1378 RecordedUserPreferences.NEW_WALL_PATTERN                          = "newWallPattern";
1379 RecordedUserPreferences.NEW_WALL_THICKNESS                        = "newWallThickness";
1380 RecordedUserPreferences.NEW_WALL_HEIGHT                           = "newHomeWallHeight";
1381 RecordedUserPreferences.NEW_WALL_BASEBOARD_THICKNESS              = "newWallBaseboardThickness";
1382 RecordedUserPreferences.NEW_WALL_BASEBOARD_HEIGHT                 = "newWallBaseboardHeight";
1383 RecordedUserPreferences.NEW_FLOOR_THICKNESS                       = "newFloorThickness";
1384 RecordedUserPreferences.AUTO_SAVE_DELAY_FOR_RECOVERY              = "autoSaveDelayForRecovery";
1385 RecordedUserPreferences.RECENT_HOMES                              = "recentHomes#";
1386 RecordedUserPreferences.IGNORED_ACTION_TIP                        = "ignoredActionTip#";
1387 RecordedUserPreferences.TEXTURE_NAME                              = "textureName#";
1388 RecordedUserPreferences.TEXTURE_CREATOR                           = "textureCreator#";
1389 RecordedUserPreferences.TEXTURE_CATEGORY                          = "textureCategory#";
1390 RecordedUserPreferences.TEXTURE_IMAGE                             = "textureImage#";
1391 RecordedUserPreferences.TEXTURE_WIDTH                             = "textureWidth#";
1392 RecordedUserPreferences.TEXTURE_HEIGHT                            = "textureHeight#";
1393 
1394 /**
1395  * Returns value of property in properties map, and return defaultValue if value is null or undefined.
1396  * @param {string, string} properties
1397  * @param {string} propertyKey
1398  * @param {any} [defaultValue]
1399  * @return {any} property's value
1400  * @private
1401  */
1402 RecordedUserPreferences.prototype.getProperty = function(properties, propertyKey, defaultValue) {
1403   if (properties[propertyKey] === undefined || properties[propertyKey] === null) {
1404     return defaultValue;
1405   }
1406   return properties[propertyKey];
1407 }
1408 
1409 /**
1410  * Returns preferences internal properties.
1411  * @return {string, string}
1412  * @private
1413  */
1414 RecordedUserPreferences.prototype.getProperties = function() {
1415   return this.properties;
1416 }
1417 
1418 /**
1419  * Sets value of a property in properties map.
1420  * @param {string, string} properties
1421  * @param {string} propertyKey
1422  * @param {any} propertyValue
1423  * @private
1424  */
1425 RecordedUserPreferences.prototype.setProperty = function(properties, propertyKey, propertyValue) {
1426   properties[propertyKey] = propertyValue;
1427 }
1428 
1429 /**
1430  * Removes the given property in properties map.
1431  * @param {string, string} properties
1432  * @param {string} propertyKey
1433  * @private
1434  */
1435 RecordedUserPreferences.prototype.removeProperty = function(properties, propertyKey) {
1436   delete properties[propertyKey];
1437 }
1438 
1439 /**
1440  * Updates saved preferences from the given properties.
1441  * @param {string, string} properties 
1442  * @param {string}         defaultUserLanguage
1443  * @param {boolean}        updateCatalogs  
1444  * @private
1445  */
1446 RecordedUserPreferences.prototype.updatePreferencesFromProperties = function(properties, defaultUserLanguage, updateCatalogs) {
1447   this.setLanguage(this.getProperty(properties, RecordedUserPreferences.LANGUAGE, defaultUserLanguage));
1448 
1449   // Read default furniture and textures catalog
1450   if (updateCatalogs) {
1451     this.updateDefaultCatalogs();
1452   }
1453   this.readModifiableTexturesCatalog(properties);
1454 
1455   var defaultPreferences = new DefaultUserPreferences(false, defaultUserLanguage);
1456   defaultPreferences.setLanguage(this.getLanguage());
1457 
1458   // Fill default patterns catalog
1459   var patternsCatalog = defaultPreferences.getPatternsCatalog();
1460   this.setPatternsCatalog(patternsCatalog);
1461 
1462   // Read other preferences
1463   var unit = LengthUnit[this.getProperty(properties, RecordedUserPreferences.UNIT)];
1464   if (!unit) {
1465     unit = defaultPreferences.getLengthUnit();
1466   }
1467   this.setUnit(unit);
1468 
1469   this.setCurrency(this.getProperty(properties, RecordedUserPreferences.CURRENCY, defaultPreferences.getCurrency()));
1470   this.setValueAddedTaxEnabled(
1471       this.getProperty(properties, RecordedUserPreferences.VALUE_ADDED_TAX_ENABLED, 
1472           '' + defaultPreferences.isValueAddedTaxEnabled()) == 'true');
1473   var percentage = this.getProperty(properties, RecordedUserPreferences.DEFAULT_VALUE_ADDED_TAX_PERCENTAGE);
1474   var valueAddedTaxPercentage = defaultPreferences.getDefaultValueAddedTaxPercentage();
1475   if (percentage !== null) {
1476     try {
1477       valueAddedTaxPercentage = new Big(percentage);
1478     } catch (ex) {
1479     }
1480   }
1481   this.setDefaultValueAddedTaxPercentage(valueAddedTaxPercentage);
1482   this.setFurnitureCatalogViewedInTree(
1483       this.getProperty(properties, RecordedUserPreferences.FURNITURE_CATALOG_VIEWED_IN_TREE, 
1484           '' + defaultPreferences.isFurnitureCatalogViewedInTree()) == 'true');
1485   this.setNavigationPanelVisible(
1486       this.getProperty(properties, RecordedUserPreferences.NAVIGATION_PANEL_VISIBLE, 
1487           '' + defaultPreferences.isNavigationPanelVisible()) == 'true');
1488   this.setEditingIn3DViewEnabled(
1489       this.getProperty(properties, RecordedUserPreferences.EDITING_IN_3D_VIEW_ENABLED, 
1490           '' + defaultPreferences.isEditingIn3DViewEnabled()) == 'true');
1491   this.setAerialViewCenteredOnSelectionEnabled(
1492       this.getProperty(properties, RecordedUserPreferences.AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED,
1493           '' + defaultPreferences.isAerialViewCenteredOnSelectionEnabled()) == 'true');
1494   this.setObserverCameraSelectedAtChange(
1495       this.getProperty(properties, RecordedUserPreferences.OBSERVER_CAMERA_SELECTED_AT_CHANGE, 
1496           '' + defaultPreferences.isObserverCameraSelectedAtChange()) == 'true');
1497   this.setMagnetismEnabled(
1498       this.getProperty(properties, RecordedUserPreferences.MAGNETISM_ENABLED, 'true') == 'true');
1499   this.setRulersVisible(
1500       this.getProperty(properties, RecordedUserPreferences.RULERS_VISIBLE, '' + defaultPreferences.isMagnetismEnabled()) == 'true');
1501   this.setGridVisible(
1502       this.getProperty(properties, RecordedUserPreferences.GRID_VISIBLE, '' + defaultPreferences.isGridVisible()) == 'true');
1503   this.setDefaultFontName(this.getProperty(properties, RecordedUserPreferences.DEFAULT_FONT_NAME, defaultPreferences.getDefaultFontName()));
1504   this.setFurnitureViewedFromTop(
1505       this.getProperty(properties, RecordedUserPreferences.FURNITURE_VIEWED_FROM_TOP, 
1506           '' + defaultPreferences.isFurnitureViewedFromTop()) == 'true');
1507   this.setFurnitureModelIconSize(parseInt(this.getProperty(properties, RecordedUserPreferences.FURNITURE_MODEL_ICON_SIZE,  
1508       '' + defaultPreferences.getFurnitureModelIconSize())));
1509   this.setFloorColoredOrTextured(
1510       this.getProperty(properties, RecordedUserPreferences.ROOM_FLOOR_COLORED_OR_TEXTURED, 
1511           '' + defaultPreferences.isRoomFloorColoredOrTextured()) == 'true');
1512 
1513   try {
1514     this.setWallPattern(patternsCatalog.getPattern(this.getProperty(properties, RecordedUserPreferences.WALL_PATTERN, 
1515         defaultPreferences.getWallPattern().getName())));
1516   } catch (ex) {
1517     // Ensure wall pattern always exists even if new patterns are added in future versions
1518     this.setWallPattern(defaultPreferences.getWallPattern());
1519   }
1520 
1521   try {
1522     if (defaultPreferences.getNewWallPattern() != null) {
1523       this.setNewWallPattern(patternsCatalog.getPattern(this.getProperty(properties, RecordedUserPreferences.NEW_WALL_PATTERN,
1524           defaultPreferences.getNewWallPattern().getName())));
1525     }
1526   } catch (ex) {
1527     // Keep new wall pattern unchanged
1528   }
1529 
1530   this.setNewWallThickness(parseFloat(this.getProperty(properties, RecordedUserPreferences.NEW_WALL_THICKNESS,
1531       '' + defaultPreferences.getNewWallThickness())));
1532   this.setNewWallHeight(parseFloat(this.getProperty(properties, RecordedUserPreferences.NEW_WALL_HEIGHT,
1533       '' + defaultPreferences.getNewWallHeight())));
1534   this.setNewWallBaseboardThickness(defaultPreferences.getNewWallBaseboardThickness());
1535   this.setNewWallBaseboardHeight(defaultPreferences.getNewWallBaseboardHeight());
1536   this.setNewWallBaseboardThickness(parseFloat(this.getProperty(properties, RecordedUserPreferences.NEW_WALL_BASEBOARD_THICKNESS,
1537       '' + defaultPreferences.getNewWallBaseboardThickness())));
1538   this.setNewWallBaseboardHeight(parseFloat(this.getProperty(properties, RecordedUserPreferences.NEW_WALL_BASEBOARD_HEIGHT,
1539       '' + defaultPreferences.getNewWallBaseboardHeight())));
1540   this.setNewFloorThickness(parseFloat(this.getProperty(properties, RecordedUserPreferences.NEW_FLOOR_THICKNESS,
1541       '' + defaultPreferences.getNewFloorThickness())));
1542   this.setAutoSaveDelayForRecovery(parseInt(this.getProperty(properties, RecordedUserPreferences.AUTO_SAVE_DELAY_FOR_RECOVERY,
1543        '' + defaultPreferences.getAutoSaveDelayForRecovery())));
1544   // Read recent homes list
1545   var recentHomes = [];
1546   for (var i = 1; i <= this.getRecentHomesMaxCount(); i++) {
1547     var recentHome = this.getProperty(properties, RecordedUserPreferences.RECENT_HOMES + i);
1548     if (recentHome != null) {
1549       recentHomes.push(recentHome);
1550     }
1551   }
1552   this.setRecentHomes(recentHomes);
1553 
1554   // Read ignored action tips
1555   for (var i = 1; ; i++) {
1556     var ignoredActionTip = this.getProperty(properties, RecordedUserPreferences.IGNORED_ACTION_TIP + i, "");
1557     if (ignoredActionTip.length == 0) {
1558       break;
1559     } else {
1560       this.ignoredActionTips[ignoredActionTip] = true;
1561     }
1562   }
1563 }
1564 
1565 /**
1566  * Adds listeners to update catalogs and follow properties to save.
1567  * @private
1568  */
1569 RecordedUserPreferences.prototype.addListeners = function() {
1570   var preferences = this;
1571   this.addPropertyChangeListener("LANGUAGE", function() {
1572       preferences.updateDefaultCatalogs();
1573     });
1574 
1575   // Add a listener to track written properties and ignore the other ones during a call to write
1576   var savedPropertyListener = function() {
1577       preferences.writtenPropertiesUpdated = true;
1578     };
1579   var writtenProperties = ["LANGUAGE", "UNIT", "CURRENCY", "VALUE_ADDED_TAX_ENABLED", "DEFAULT_VALUE_ADDED_TAX_PERCENTAGE",
1580       "FURNITURE_CATALOG_VIEWED_IN_TREE", "NAVIGATION_PANEL_VISIBLE", "EDITING_IN_3D_VIEW_ENABLED", "AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED",
1581       "OBSERVER_CAMERA_SELECTED_AT_CHANGE", "MAGNETISM_ENABLED", "GRID_VISIBLE", "DEFAULT_FONT_NAME", "FURNITURE_VIEWED_FROM_TOP",
1582       "FURNITURE_MODEL_ICON_SIZE", "ROOM_FLOOR_COLORED_OR_TEXTURED", "NEW_WALL_PATTERN", "NEW_WALL_THICKNESS",
1583       "NEW_WALL_HEIGHT", "NEW_WALL_BASEBOARD_THICKNESS", "NEW_WALL_BASEBOARD_HEIGHT", "NEW_FLOOR_THICKNESS"];
1584   for (var i = 0; i < writtenProperties.length; i++) {
1585     var writtenProperty = writtenProperties[i];
1586     this.addPropertyChangeListener(writtenProperty, savedPropertyListener);
1587   }
1588   this.getTexturesCatalog().addTexturesListener(savedPropertyListener);
1589 }
1590 
1591 /**
1592  * Read preferences properties from backend.
1593  * @param {string, string} properties
1594  * @private
1595  */
1596 RecordedUserPreferences.prototype.readPreferences = function(properties, defaultUserLanguage) {
1597   try {
1598     var preferences = this;
1599     var updateJsonPreferences = function(jsonPreferences) {
1600         if (jsonPreferences != null) {
1601           var preferencesData = JSON.parse(jsonPreferences);
1602           for (var i in preferencesData) {
1603             properties [i] = preferencesData [i];
1604           }
1605         } 
1606         preferences.updatePreferencesFromProperties(properties, defaultUserLanguage, true);
1607         preferences.addListeners();
1608       };
1609       
1610     if (this.readPreferencesUrl.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) === 0) {
1611       var key = this.readPreferencesUrl.substring(LocalStorageURLContent.LOCAL_STORAGE_PREFIX.length);
1612       updateJsonPreferences(localStorage.getItem(key));
1613     } else if (this.readPreferencesUrl.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) === 0) {
1614       new IndexedDBURLContent(this.readPreferencesUrl).getBlob({
1615           blobReady: function(blob) {
1616             var reader = new FileReader();
1617             // Use onload rather that addEventListener for Cordova support
1618             reader.onload = function() {
1619                 updateJsonPreferences(reader.result);
1620               };
1621             reader.readAsText(blob);
1622           },
1623           blobError : function(status, error) {
1624             preferences.updateDefaultCatalogs();
1625             preferences.addListeners();
1626             if (status != -1) {
1627               console.log("Can't read preferences from indexedDB " + status + " " + error);
1628             }
1629           }
1630         });
1631     } else {
1632       var request = new XMLHttpRequest();
1633       var querySeparator = this.readPreferencesUrl.indexOf('?') != -1 ? '&' : '?';
1634       request.open("GET", this.readPreferencesUrl + querySeparator + "requestId=" + UUID.randomUUID(), true); 
1635       request.addEventListener("load", function() {
1636           if (request.readyState === XMLHttpRequest.DONE
1637               && request.status === 200) {
1638             updateJsonPreferences(request.responseText);
1639           } else {
1640             preferences.updateDefaultCatalogs();
1641             preferences.addListeners();
1642           }
1643         });
1644       var errorListener = function(ev) {
1645           console.log("Can't read preferences from server");
1646           preferences.updateDefaultCatalogs();
1647           preferences.addListeners();
1648         };
1649       request.addEventListener("error", errorListener);
1650       request.addEventListener("timeout", errorListener);
1651       request.timeout = 10000;
1652       request.send();
1653     }
1654   } catch (ex) {
1655     console.log(ex);
1656     preferences.updateDefaultCatalogs();    
1657     preferences.addListeners();
1658   }
1659 }
1660 
1661 /**
1662  * @private
1663  */
1664 RecordedUserPreferences.prototype.updateDefaultCatalogs = function() {
1665   // Delete default pieces of current furniture catalog
1666   var furnitureCatalog = this.getFurnitureCatalog();
1667   for (var i = furnitureCatalog.getCategories().length - 1; i >= 0; i--) {
1668     var category = furnitureCatalog.getCategory(i);
1669     for (var j = category.getFurniture().length - 1; j >= 0; j--) {
1670       var piece = category.getPieceOfFurniture(j);
1671       if (!piece.isModifiable()) {
1672         furnitureCatalog['delete'](piece); // Can't call delete method directly because delete is a JS reserved word 
1673       }
1674     }
1675   }
1676 
1677   // Add default pieces
1678   var resourceFurnitureCatalog = typeof DefaultFurnitureCatalog === "function"
1679       ? (Array.isArray(this.furnitureCatalogUrls)
1680           ? this.readFurnitureCatalogFromResource(this.furnitureCatalogUrls, this.furnitureResourcesUrlBase)
1681           : new DefaultFurnitureCatalog(this))
1682       : new FurnitureCatalog();
1683   for (var i = 0; i < resourceFurnitureCatalog.getCategories().length; i++) {
1684     var category = resourceFurnitureCatalog.getCategory(i);
1685     for (var j = 0; j < category.getFurniture().length; j++) {
1686       var piece = category.getPieceOfFurniture(j);
1687       furnitureCatalog.add(category, piece);
1688     }
1689   }
1690 
1691   // Delete default textures of current textures catalog
1692   var texturesCatalog = this.getTexturesCatalog();
1693   for (var i = texturesCatalog.getCategories().length - 1; i >= 0; i--) {
1694     var category = texturesCatalog.getCategory(i);
1695     for (var j = category.getTextures().length - 1; j >= 0; j--) {
1696       var texture = category.getTexture(j);
1697       if (!texture.isModifiable()) {
1698         texturesCatalog['delete'](texture);
1699       }
1700     }
1701   }
1702   
1703   // Add default textures
1704   var resourceTexturesCatalog = typeof DefaultTexturesCatalog === "function"
1705       ? (Array.isArray(this.texturesCatalogUrls)
1706           ? this.readTexturesCatalogFromResource(this.texturesCatalogUrls, this.texturesResourcesUrlBase)
1707           : new DefaultTexturesCatalog(this))
1708       : new TexturesCatalog();
1709 
1710   for (var i = 0; i < resourceTexturesCatalog.getCategories().length; i++) {
1711     var category = resourceTexturesCatalog.getCategory(i);
1712     for (var j = 0; j < category.getTextures().length; j++) {
1713       var texture = category.getTexture(j);
1714       texturesCatalog.add(category, texture);
1715     }
1716   }
1717 }
1718 
1719 /**
1720  * Returns the furniture catalog contained from the given resources.
1721  * @param {Array} furnitureCatalogUrls
1722  * @param {String} furnitureResourcesUrlBase
1723  * @protected
1724  */
1725 RecordedUserPreferences.prototype.readFurnitureCatalogFromResource = function(furnitureCatalogUrls, furnitureResourcesUrlBase) {
1726   return new DefaultFurnitureCatalog(furnitureCatalogUrls, furnitureResourcesUrlBase);
1727 }
1728 
1729 /**
1730  * Returns the textures catalog contained from the the given resources.
1731  * @param {Array} texturesCatalogUrls
1732  * @param {String} texturesResourcesUrlBase
1733  * @protected
1734  */
1735 RecordedUserPreferences.prototype.readTexturesCatalogFromResource = function(texturesCatalogUrls, texturesResourcesUrlBase) {
1736   return new DefaultTexturesCatalog(texturesCatalogUrls, texturesResourcesUrlBase);
1737 }
1738 
1739 /**
1740  * Reads modifiable textures catalog from preferences.
1741  * @param {string, string} properties 
1742  * @private
1743  */
1744 RecordedUserPreferences.prototype.readModifiableTexturesCatalog = function(properties) {
1745   var texture;
1746   for (var i = 1; (texture = this.readModifiableTexture(properties, i)) != null; i++) {
1747     if (texture.getImage().getURL() != "") {
1748       var textureCategory = this.readModifiableTextureCategory(properties, i);
1749       this.getTexturesCatalog().add(textureCategory, texture);
1750     }
1751   }
1752 }
1753 
1754 /**
1755  * Returns the modifiable texture read from <code>properties</code> at the given <code>index</code>.
1756  * @param {string, string} properties 
1757  * @param {number} index  the index of the read texture
1758  * @return the read texture or <code>null</code> if the texture at the given index doesn't exist.
1759  * @protected
1760  */
1761 RecordedUserPreferences.prototype.readModifiableTexture = function(properties, index) {
1762   var name = this.getProperty(properties, RecordedUserPreferences.TEXTURE_NAME + index, null);
1763   if (name == null) {
1764     // Return null if key textureName# doesn't exist
1765     return null;
1766   }
1767   var image = URLContent.fromURL(this.getProperty(properties, RecordedUserPreferences.TEXTURE_IMAGE + index, ""));
1768   var width = parseFloat(this.getProperty(properties, RecordedUserPreferences.TEXTURE_WIDTH + index, "0.1"));
1769   var height = parseFloat(this.getProperty(properties, RecordedUserPreferences.TEXTURE_HEIGHT + index, "0.1"));
1770   var creator = this.getProperty(properties, RecordedUserPreferences.TEXTURE_CREATOR + index, null);
1771   return new CatalogTexture(null, name, image, width, height, creator, true);
1772 }
1773 
1774 /**
1775 * Returns the category of a texture at the given <code>index</code>
1776 * read from <code>properties</code>.
1777  * @param {string, string} properties 
1778  * @param {number} index  the index of the read texture
1779  * @protected
1780 */
1781 RecordedUserPreferences.prototype.readModifiableTextureCategory = function(properties, index) {
1782   var category = this.getProperty(properties, RecordedUserPreferences.TEXTURE_CATEGORY + index, "");
1783   return new TexturesCategory(category);
1784 }
1785 
1786 /**
1787  * Writes user preferences to properties, and sends to the <code>writePreferencesUrl</code> (if
1788  * given at the creation) a JSON content describing preferences.
1789  */
1790 RecordedUserPreferences.prototype.write = function() {
1791   UserPreferences.prototype.write.call(this);
1792 
1793   // Write actually preferences only if written properties were updated
1794   if (this.writtenPropertiesUpdated) {
1795     var properties = this.getProperties();
1796     this.writeModifiableTexturesCatalog(properties);
1797    
1798     // Write other preferences
1799     this.setProperty(properties, RecordedUserPreferences.LANGUAGE, this.getLanguage());
1800     this.setProperty(properties, RecordedUserPreferences.UNIT, this.getLengthUnit().name());
1801     var currency = this.getCurrency();
1802     if (currency === null) {
1803       this.removeProperty(properties, RecordedUserPreferences.CURRENCY);
1804     } else {
1805       this.setProperty(properties, RecordedUserPreferences.CURRENCY, currency);
1806     }
1807     this.setProperty(properties, RecordedUserPreferences.VALUE_ADDED_TAX_ENABLED, '' + this.isValueAddedTaxEnabled());
1808     var valueAddedTaxPercentage = this.getDefaultValueAddedTaxPercentage();
1809     if (valueAddedTaxPercentage === null) {
1810       this.removeProperty(properties, RecordedUserPreferences.DEFAULT_VALUE_ADDED_TAX_PERCENTAGE);
1811     } else {
1812       this.setProperty(properties, RecordedUserPreferences.DEFAULT_VALUE_ADDED_TAX_PERCENTAGE, valueAddedTaxPercentage.toString());
1813     }
1814     this.setProperty(properties, RecordedUserPreferences.FURNITURE_CATALOG_VIEWED_IN_TREE, '' + this.isFurnitureCatalogViewedInTree());
1815     this.setProperty(properties, RecordedUserPreferences.NAVIGATION_PANEL_VISIBLE, '' + this.isNavigationPanelVisible());
1816     this.setProperty(properties, RecordedUserPreferences.EDITING_IN_3D_VIEW_ENABLED, '' + this.isEditingIn3DViewEnabled());
1817     this.setProperty(properties, RecordedUserPreferences.AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED, '' + this.isAerialViewCenteredOnSelectionEnabled());
1818     this.setProperty(properties, RecordedUserPreferences.OBSERVER_CAMERA_SELECTED_AT_CHANGE, '' + this.isObserverCameraSelectedAtChange());
1819     this.setProperty(properties, RecordedUserPreferences.MAGNETISM_ENABLED, '' + this.isMagnetismEnabled());
1820     this.setProperty(properties, RecordedUserPreferences.RULERS_VISIBLE, '' + this.isRulersVisible());
1821     this.setProperty(properties, RecordedUserPreferences.GRID_VISIBLE, '' + this.isGridVisible());
1822     var defaultFontName = this.getDefaultFontName();
1823     if (defaultFontName == null) {
1824       this.removeProperty(properties, RecordedUserPreferences.DEFAULT_FONT_NAME);
1825     } else {
1826       this.setProperty(properties, RecordedUserPreferences.DEFAULT_FONT_NAME, defaultFontName);
1827     }
1828     this.setProperty(properties, RecordedUserPreferences.FURNITURE_VIEWED_FROM_TOP, '' + this.isFurnitureViewedFromTop());
1829     this.setProperty(properties, RecordedUserPreferences.FURNITURE_MODEL_ICON_SIZE, '' + this.getFurnitureModelIconSize());
1830     this.setProperty(properties, RecordedUserPreferences.ROOM_FLOOR_COLORED_OR_TEXTURED, '' + this.isRoomFloorColoredOrTextured());
1831     this.setProperty(properties, RecordedUserPreferences.WALL_PATTERN, this.getWallPattern().getName());
1832     var newWallPattern = this.getNewWallPattern();
1833     if (newWallPattern != null) {
1834       this.setProperty(properties, RecordedUserPreferences.NEW_WALL_PATTERN, newWallPattern.getName());
1835     }
1836     this.setProperty(properties, RecordedUserPreferences.NEW_WALL_THICKNESS, '' + this.getNewWallThickness());
1837     this.setProperty(properties, RecordedUserPreferences.NEW_WALL_HEIGHT, '' + this.getNewWallHeight());
1838     this.setProperty(properties, RecordedUserPreferences.NEW_WALL_BASEBOARD_THICKNESS, '' + this.getNewWallBaseboardThickness());
1839     this.setProperty(properties, RecordedUserPreferences.NEW_WALL_BASEBOARD_HEIGHT, '' + this.getNewWallBaseboardHeight());
1840     this.setProperty(properties, RecordedUserPreferences.NEW_FLOOR_THICKNESS, '' + this.getNewFloorThickness());
1841     this.setProperty(properties, RecordedUserPreferences.AUTO_SAVE_DELAY_FOR_RECOVERY, '' + this.getAutoSaveDelayForRecovery());
1842      // Write recent homes list
1843     var recentHomes = this.getRecentHomes();
1844     for (var i = 0; i < recentHomes.length && i < this.getRecentHomesMaxCount(); i++) {
1845       this.setProperty(properties, RecordedUserPreferences.RECENT_HOMES + (i + 1), recentHomes[i]);
1846     }
1847     // Write ignored action tips
1848     var ignoredActionTipsKeys = Object.keys(this.ignoredActionTips);
1849     for (var i = 0; i < ignoredActionTipsKeys.length; i++) {
1850       var key = ignoredActionTipsKeys[i];
1851       if (this.ignoredActionTips[key]) {
1852         this.setProperty(properties, RecordedUserPreferences.IGNORED_ACTION_TIP + (i + 1), key);
1853       }
1854     }
1855 
1856     if (Object.keys(this.uploadingBlobs).length > 0) {
1857       var preferences = this;
1858       // Wait blobs uploading end before trying to write preferences referencing them
1859       setTimeout(function() {
1860           preferences.write();
1861         }, 1000);
1862     } else {
1863       this.writtenPropertiesUpdated = false;
1864       this.writePreferences(properties);
1865     }
1866   }
1867 }
1868 
1869 /**
1870  * Sends user preferences stored in properties to backend.
1871  * @param {string, string} properties
1872  * @private
1873  */
1874 RecordedUserPreferences.prototype.writePreferences = function(properties) {
1875   if (this.writePreferencesUrl) {
1876     var preferences = this;
1877     if (this.writingPreferences) {
1878       // Avoid writing preferences twice at the same time
1879       setTimeout(function() {
1880           preferences.writePreferences(properties);
1881         }, 100);
1882     } else {
1883       this.writingPreferences = true;
1884       var jsonPreferences = JSON.stringify(properties);
1885       var successHandler = function() {
1886           if (preferences.writingObserver !== undefined
1887                 && preferences.writingObserver.writeSucceeded) {
1888             preferences.writingObserver.writeSucceeded(properties);
1889           }
1890           setTimeout(function() {
1891               delete preferences.writingPreferences;
1892             }, 500);
1893         };
1894       var errorHandler = function(status, error) {
1895           if (preferences.writingObserver !== undefined
1896               && preferences.writingObserver.writeFailed) {
1897             preferences.writingObserver.writeFailed(properties, status, error);
1898           }
1899           setTimeout(function() {
1900               delete preferences.writingPreferences;
1901               // Retry
1902               preferences.writePreferences(properties);
1903             }, 10000);
1904         };
1905         
1906       if (this.writePreferencesUrl.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) === 0) {
1907         try {
1908           var key = this.writePreferencesUrl.substring(LocalStorageURLContent.LOCAL_STORAGE_PREFIX.length);
1909           localStorage.setItem(key, jsonPreferences);
1910           successHandler();
1911           delete preferences.writingPreferences;
1912         } catch (ex) {
1913           errorHandler(ex, ex.message);
1914         }
1915       } else if (this.writePreferencesUrl.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) === 0) {
1916         var preferencesContent = new BlobURLContent(new Blob([jsonPreferences], { type: 'application/json' }));
1917         preferencesContent.writeBlob(this.writePreferencesUrl, "", {
1918             blobSaved: function(content, name) {
1919               URL.revokeObjectURL(preferencesContent.getURL());
1920               successHandler();
1921             },
1922             blobError : function(status, error) {
1923               URL.revokeObjectURL(preferencesContent.getURL());
1924               errorHandler(status, error);
1925             }
1926          });
1927       } else {
1928         var request = new XMLHttpRequest();
1929         var querySeparator = this.writePreferencesUrl.indexOf('?') != -1 ? '&' : '?';
1930         request.open("POST", this.writePreferencesUrl + querySeparator + "updateId=" + UUID.randomUUID(), true);
1931         request.addEventListener('load', function (ev) {
1932             if (request.readyState === XMLHttpRequest.DONE) {
1933               if (request.status === 200) {
1934                 successHandler();
1935               } else {
1936                 errorHandler(request.status, request.responseText);
1937               }
1938             }
1939           });
1940         var errorListener = function(ev) {
1941             errorHandler(0, "Can't post " + preferences.writePreferencesUrl);
1942           };
1943         request.addEventListener("error", errorListener);
1944         request.addEventListener("timeout", errorListener);
1945         request.send(jsonPreferences);
1946       }
1947     }
1948   }
1949 }
1950 
1951 /**
1952  * Sets which action tip should be ignored.
1953  * @ignore
1954  */
1955 RecordedUserPreferences.prototype.setActionTipIgnored = function(actionKey) {
1956   this.ignoredActionTips[actionKey] = true;
1957   UserPreferences.prototype.setActionTipIgnored.call(this, actionKey);
1958 }
1959 
1960 /**
1961  * Returns whether an action tip should be ignored or not.
1962  * @ignore
1963  */
1964 RecordedUserPreferences.prototype.isActionTipIgnored = function(actionKey) {
1965   var ignoredActionTip = this.ignoredActionTips[actionKey];
1966   return ignoredActionTip === true;
1967 }
1968 
1969 /**
1970  * Resets the display flag of action tips.
1971  * @ignore
1972  */
1973 RecordedUserPreferences.prototype.resetIgnoredActionTips = function() {
1974   var keys = Object.keys(this.ignoredActionTips);
1975   for (var i = 0; i < keys.length; i++) {
1976     this.ignoredActionTips[keys[i]] = false;
1977   }
1978   UserPreferences.prototype.resetIgnoredActionTips.call(this);
1979 }
1980 
1981 /**
1982  * Throws an exception because these user preferences can't manage language libraries.
1983  * @ignore
1984  */
1985 RecordedUserPreferences.prototype.addLanguageLibrary = function(location) {
1986   throw new UnsupportedOperationException();
1987 }
1988 
1989 /**
1990  * Throws an exception because these user preferences can't manage additional language libraries.
1991  * @ignore
1992  */
1993 RecordedUserPreferences.prototype.languageLibraryExists = function(location) {
1994   throw new UnsupportedOperationException();
1995 }
1996 
1997 /**
1998  * Returns <code>true</code> if the furniture library at the given <code>location</code> exists.
1999  * @ignore
2000  */
2001 RecordedUserPreferences.prototype.furnitureLibraryExists = function(location) {
2002   throw new UnsupportedOperationException();
2003 }
2004 
2005 /**
2006  * Throws an exception because these user preferences can't manage additional furniture libraries.
2007  * @ignore
2008  */
2009 RecordedUserPreferences.prototype.addFurnitureLibrary = function(location) {
2010   throw new UnsupportedOperationException();
2011 }
2012 
2013 /**
2014  * Returns <code>true</code> if the textures library at the given <code>location</code> exists.
2015  * @ignore
2016  */
2017 RecordedUserPreferences.prototype.texturesLibraryExists = function(location) {
2018   throw new UnsupportedOperationException();
2019 }
2020 
2021 /**
2022  * Throws an exception because these user preferences can't manage additional textures libraries.
2023  * @ignore
2024  */
2025 RecordedUserPreferences.prototype.addTexturesLibrary = function(location) {
2026   throw new UnsupportedOperationException();
2027 }
2028 
2029 /**
2030  * Throws an exception because these user preferences don't manage additional libraries.
2031  * @ignore
2032  */
2033 RecordedUserPreferences.prototype.getLibraries = function() {
2034   throw new UnsupportedOperationException();
2035 }
2036 
2037 /**
2038  * Save modifiable textures to catalog.json and upload new resources.
2039  * @param {string, string} properties 
2040  * @private
2041  */
2042 RecordedUserPreferences.prototype.writeModifiableTexturesCatalog = function(properties) {
2043   if (this.writeResourceUrl && this.readResourceUrl) {
2044     var index = 1;
2045     var texturesCatalog = this.getTexturesCatalog();
2046     var preferences = this;
2047     for (var i = 0; i < texturesCatalog.getCategoriesCount(); i++) {
2048       var textureCategory = texturesCatalog.getCategory(i);
2049       for (var j = 0; j < textureCategory.getTexturesCount(); j++) {
2050         var catalogTexture = textureCategory.getTexture(j);
2051         var textureImage = catalogTexture.getImage();
2052         if (catalogTexture.isModifiable() 
2053             && textureImage instanceof URLContent) {
2054           this.setProperty(properties, RecordedUserPreferences.TEXTURE_NAME + index, catalogTexture.getName());
2055           this.setProperty(properties, RecordedUserPreferences.TEXTURE_CATEGORY + index, textureCategory.getName());
2056           this.setProperty(properties, RecordedUserPreferences.TEXTURE_WIDTH + index, catalogTexture.getWidth());
2057           this.setProperty(properties, RecordedUserPreferences.TEXTURE_HEIGHT + index, catalogTexture.getHeight());
2058           if (catalogTexture.getCreator() != null) {
2059             this.setProperty(properties, RecordedUserPreferences.TEXTURE_CREATOR + index, catalogTexture.getCreator());
2060           } else {
2061             this.removeProperty(properties, RecordedUserPreferences.TEXTURE_CREATOR + index);
2062           }
2063   
2064           if (textureImage instanceof LocalURLContent
2065               && (!(textureImage instanceof LocalStorageURLContent)
2066                   || this.writeResourceUrl.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) < 0)
2067               && (!(textureImage instanceof IndexedDBURLContent)
2068                   || this.writeResourceUrl.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) < 0)) {
2069             if (!this.isSavedContentInResourceScope(textureImage)) {
2070               var textureImageFileName = this.uploadingBlobs[textureImage.getURL()];
2071               if (textureImageFileName === undefined) {
2072                 textureImage.getBlob({
2073                     textureImage: textureImage, 
2074                     blobReady: function(blob) {
2075                       textureImageFileName = UUID.randomUUID();
2076                       preferences.uploadingBlobs[this.textureImage.getURL()] = textureImageFileName;
2077                       var imageExtension = blob.type == "image/png" ? ".png" : ".jpg";
2078                       var loadListener = function(textureImage, fileName, textureIndex) {
2079                           if (!preferences.isSavedContentInResourceScope(textureImage)) {
2080                             var savedContent = URLContent.fromURL(
2081                                 CoreTools.format(preferences.readResourceUrl.replace(/(%[^s])/g, "%$1"), encodeURIComponent(fileName)));
2082                             textureImage.setSavedContent(savedContent);
2083                           }
2084                           delete preferences.uploadingBlobs[textureImage.getURL()];
2085                           preferences.setProperty(properties, RecordedUserPreferences.TEXTURE_IMAGE + textureIndex, textureImage.getSavedContent().getURL());
2086                         };
2087                       preferences.writeResource(textureImage, textureImageFileName + imageExtension, index, loadListener);
2088                     },
2089                     blobError: function(status, error) {
2090                       contentsObserver.resourcesError(status, error);
2091                     }
2092                 });    
2093               }
2094             } else {
2095               // Always update uploading blobs map because blob may have been saved elsewhere
2096               delete preferences.uploadingBlobs[textureImage.getURL()];
2097               this.setProperty(properties, RecordedUserPreferences.TEXTURE_IMAGE + index, textureImage.getSavedContent().getURL());
2098             }
2099           } else if (textureImage instanceof URLContent) {
2100             this.setProperty(properties, RecordedUserPreferences.TEXTURE_IMAGE + index, textureImage.getURL());
2101           }
2102           index++;
2103         }
2104       }
2105     }
2106     
2107     // Remove obsolete keys
2108     for ( ; this.getProperty(properties, RecordedUserPreferences.TEXTURE_NAME + index, null) != null; index++) {
2109       this.removeProperty(properties, RecordedUserPreferences.TEXTURE_NAME + index);
2110       this.removeProperty(properties, RecordedUserPreferences.TEXTURE_IMAGE + index);
2111       this.removeProperty(properties, RecordedUserPreferences.TEXTURE_CATEGORY + index);
2112       this.removeProperty(properties, RecordedUserPreferences.TEXTURE_WIDTH + index);
2113       this.removeProperty(properties, RecordedUserPreferences.TEXTURE_HEIGHT + index);
2114       this.removeProperty(properties, RecordedUserPreferences.TEXTURE_CREATOR + index);
2115     }
2116   }
2117 }
2118 
2119 /**
2120  * Returns <code>true</code> if the saved content of the given content exists 
2121  * and depends on the scope where the resources managed by preferences are saved.
2122  * @param {URLContent} urlContent  content
2123  * @private
2124  */
2125 RecordedUserPreferences.prototype.isSavedContentInResourceScope = function(urlContent) {
2126   var savedContent = urlContent.getSavedContent();
2127   return savedContent !== null
2128       && (!(savedContent instanceof IndexedDBURLContent)
2129           || this.writeResourceUrl.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) < 0
2130           || savedContent.getURL().indexOf(this.writeResourceUrl.substring(0, this.writeResourceUrl.indexOf('?'))) === 0);
2131 }
2132 
2133 /**
2134  * @param {BlobURLContent} urlContent  blob content
2135  * @param {string} path unique file name of the written resource
2136  * @param {number} index
2137  * @param {function()} loadListener called when content is uploaded
2138  * @private
2139  */
2140 RecordedUserPreferences.prototype.writeResource = function(urlContent, path, index, loadListener) {
2141   var preferences = this;
2142   urlContent.writeBlob(this.writeResourceUrl, path, {
2143       blobSaved: function(content, name) {
2144         if (preferences.writingObserver !== undefined
2145             && preferences.writingObserver.writeSucceeded) {
2146           preferences.writingObserver.writeSucceeded(content.getBlob());
2147         }
2148         loadListener(content, path, index);
2149       },
2150       blobError : function(status, error) {
2151          if (preferences.writingObserver !== undefined
2152               && preferences.writingObserver.writeFailed) {
2153            preferences.writingObserver.writeFailed(urlContent.getBlob(), status, error);
2154          }
2155          // In case of error, wait 10s before attempting a new upload
2156          setTimeout(function() {
2157              // Check it wasn't saved elsewhere
2158              if (urlContent.getSavedContent() === null) {
2159                preferences.writeResource(urlContent, path, index, loadListener);
2160              } else {
2161                loadListener(urlContent, index);
2162              }
2163            }, 10000);
2164       }
2165     });
2166 }
2167