1 /*
  2  * JSViewFactory.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.com>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 // Requires SweetHome3D.js
 22 // Requires UserPreferences.js
 23 // Requires FurnitureCatalogListPanel.js
 24 // Requires PlanComponent.js
 25 // Requires HomeComponent3D.js
 26 // Requires HomePane.js
 27 
 28 /**
 29  * A view default factory that is use to create all the views in the application.
 30  * @constructor
 31  * @author Emmanuel Puybaret
 32  * @author Renaud Pawlak
 33  * @author Louis Grignon 
 34  */
 35 function JSViewFactory(application) {
 36   this.application = application;
 37 }
 38 JSViewFactory.prototype = Object.create(JSViewFactory.prototype);
 39 JSViewFactory.prototype.constructor = JSViewFactory;
 40 
 41 JSViewFactory["__class"] = "JSViewFactory";
 42 JSViewFactory["__interfaces"] = ["com.eteks.sweethome3d.viewcontroller.ViewFactory"];
 43 
 44 
 45 JSViewFactory.dummyDialogView = {
 46   displayView: function(parent) { 
 47     // Do nothing
 48   }
 49 }
 50 
 51 JSViewFactory.prototype.createFurnitureCatalogView = function(catalog, preferences, furnitureCatalogController) {
 52   return new FurnitureCatalogListPanel("furniture-catalog", catalog, preferences, furnitureCatalogController);
 53 }
 54 
 55 /**
 56  * @param {Home} home
 57  * @param {UserPreferences} preferences
 58  * @param {FurnitureController} controller
 59  * @return {FurnitureListPanel | undefined} undefined if DOM element #furniture-view is not found (feature is disabled)
 60  */
 61 JSViewFactory.prototype.createFurnitureView = function(home, preferences, controller) {
 62   if (document.getElementById("furniture-view") != null) {
 63     return new FurnitureTablePanel("furniture-view", home, preferences, controller);
 64   } else {
 65     return undefined;
 66   }
 67 }
 68 
 69 JSViewFactory.prototype.createPlanView = function(home, preferences, planController) {
 70   return new PlanComponent("home-plan", home, preferences, planController);
 71 }
 72 
 73 JSViewFactory.prototype.createView3D = function(home, preferences, homeController3D) {
 74   return new HomeComponent3D("home-3D-view", home, preferences, null, homeController3D);
 75 }
 76 
 77 JSViewFactory.prototype.createHomeView = function(home, preferences, homeController) {
 78   return new HomePane("home-pane", home, preferences, homeController);
 79 }
 80 
 81 /**
 82  * Returns a new view that displays a wizard.
 83  * @param {UserPreferences} preferences the current user preferences
 84  * @param {WizardController} controller wizard's controller
 85  */
 86 JSViewFactory.prototype.createWizardView = function(preferences, controller) {
 87   return new JSWizardDialog(preferences, controller,  
 88       controller.getTitle() || "@{WizardPane.wizard.title}", 
 89       {
 90       });
 91 }
 92 
 93 JSViewFactory.prototype.createBackgroundImageWizardStepsView = function(backgroundImage, preferences, controller) {
 94   var LARGE_IMAGE_PIXEL_COUNT_THRESHOLD = 10000000;
 95   var LARGE_IMAGE_MAX_PIXEL_COUNT = 8000000;
 96   var CANVAS_TOUCHABLE_AREA_RADIUS = 10;
 97 
 98   function BackgroundImageWizardStepsView() {
 99     JSComponent.call(this, preferences,
100           "<div choiceStep>"
101         + "  <div description>@{BackgroundImageWizardStepsPanel.imageChangeLabel.text}</div>"
102         + "  <div class='buttons'>"
103         + "    <button selectImage></button>"
104         + "    <input type='file' accept='image/*' style='display: none' />"
105         + "  </div>"
106         + "  <div preview>"
107         + "    <img />"
108         + "  </div>"
109         + "</div>"
110         + "<div scaleStep>"
111         + "  <div>@{BackgroundImageWizardStepsPanel.scaleLabel.text}</div>"
112         + "  <br />"
113         + "  <div>"
114         + "    <span data-name='scale-distance-label'></span>"
115         + "    <span data-name='scale-distance-input'></span>"
116         + "  </div>"
117         + "  <br />"
118         + "  <div class='preview-panel'>"
119         + "    <div preview>"
120         + "      <canvas />"
121         + "    </div>"
122         + "    <div class='preview-controls' style='z-index:5'>"
123         + "      <div previewZoomIn></div>"
124         + "      <br />"
125         + "      <div previewZoomOut></div>"
126         + "    </div>"
127         + "  </div>"
128         + "</div>"
129         + "<div originStep>"
130         + "  <div>@{BackgroundImageWizardStepsPanel.originLabel.text}</div>"
131         + "  <br />"
132         + "  <div>"
133         + "    <span data-name='x-origin-label'></span>"
134         + "    <span data-name='x-origin-input'></span>"
135         + "    <span data-name='y-origin-label'></span>"
136         + "    <span data-name='y-origin-input'></span>"
137         + "  </div>"
138         + "  <br />"
139         + "  <div class='preview-panel'>"
140         + "    <div preview>"
141         + "      <canvas />"
142         + "    </div>"
143         + "    <div class='preview-controls' style='z-index:5'>"
144         + "      <div previewZoomIn></div>"
145         + "      <br />"
146         + "      <div previewZoomOut></div>"
147         + "    </div>"
148         + "  </div>"
149         + "</div>");
150 
151     this.controller = controller;
152     this.getHTMLElement().classList.add("background-image-wizard");
153 
154     this.initImageChoiceStep();
155     this.initScaleStep();
156     this.initOriginStep();
157 
158     var component = this;
159     this.registerPropertyChangeListener(controller, "STEP", function(ev) {
160         component.updateStep();
161         component.repaintOriginCanvas();
162       });
163     this.registerPropertyChangeListener(controller, "IMAGE", function(ev) {
164         component.updatePreviewComponentsImage();
165       });
166 
167     this.updateImage(backgroundImage);
168   }
169   BackgroundImageWizardStepsView.prototype = Object.create(JSComponent.prototype);
170   BackgroundImageWizardStepsView.prototype.constructor = BackgroundImageWizardStepsView;
171   
172   BackgroundImageWizardStepsView.prototype.buildHtmlFromTemplate = function(templateHtml) {
173     return JSComponent.prototype.buildHtmlFromTemplate.call(this, templateHtml).replace(/\<br\>/g, " ");
174   }
175 
176   /**
177    * @private
178    */
179   BackgroundImageWizardStepsView.prototype.initImageChoiceStep = function () {
180     var component = this;
181     component.imageChoiceStep = {
182         panel: component.findElement("[choiceStep]"),
183         imageChoiceOrChangeLabel: component.findElement("[choiceStep] [description]"),
184         imageChoiceOrChangeButton: component.findElement("[choiceStep] [selectImage]"),
185         imageChooser: component.findElement("[choiceStep] input[type='file']"),
186         preview: component.findElement("[choiceStep] [preview] img"),
187       };
188     var imageErrorListener = function(ev) {
189         console.warn("Error loading image: " + ev);
190         component.controller.setImage(null);
191         component.setImageChoiceTexts();
192         component.updatePreviewComponentsImage();
193         alert(ResourceAction.getLocalizedLabelText(preferences, "BackgroundImageWizardStepsPanel",
194             "imageChoiceErrorLabel.text"));
195       };
196     component.registerEventListener(component.imageChoiceStep.imageChoiceOrChangeButton, "click", function(ev) {
197         component.imageChoiceStep.imageChooser.click();
198       });
199       
200     var importImage = function(file) {
201         if (file) {
202           var reader = new FileReader();
203           // Use onload and onerror rather that addEventListener for Cordova support
204           reader.onload = function(ev) {
205               var image = new Image();
206               image.addEventListener("load", function(ev) {
207                   component.updateController(image, file);
208                 });
209               image.addEventListener("error", imageErrorListener);
210               image.src = ev.target.result;
211             };
212           reader.onerror = imageErrorListener;
213           reader.readAsDataURL(file);
214         }
215       };
216     component.registerEventListener(component.imageChoiceStep.imageChooser, "input", function(ev) {
217         importImage(this.files[0]);
218       });
219     component.registerEventListener(component.imageChoiceStep.preview, "drop", function(ev) {
220         ev.preventDefault();
221         importImage(ev.dataTransfer.files[0]);
222       });
223     component.registerEventListener(component.imageChoiceStep.preview, "dragover", function(ev) {
224         ev.preventDefault();
225       });
226   }
227 
228   /**
229    * @private
230    */
231   BackgroundImageWizardStepsView.prototype.initScaleStep = function () {
232     var component = this;
233     var unitName = preferences.getLengthUnit().getName();
234     var maximumLength = preferences.getLengthUnit().getMaximumLength();
235 
236     component.scaleStep = {
237         panel: component.findElement("[scaleStep]"),
238         preview: component.findElement("[scaleStep] [preview] canvas"),
239         previewZoomIn: component.findElement("[scaleStep] [previewZoomIn]"),
240         previewZoomOut: component.findElement("[scaleStep] [previewZoomOut]"),
241         scaleDistanceLabel: component.getElement("scale-distance-label"),
242         scaleDistanceInput: new JSSpinner(preferences, component.getElement("scale-distance-input"), 
243             {
244               format: preferences.getLengthUnit().getFormat(),
245               minimum: preferences.getLengthUnit().getMinimumLength(),
246               maximum: maximumLength,
247               stepSize: preferences.getLengthUnit().getStepSize()
248             }),
249       };
250 
251     component.scaleStep.scaleDistanceLabel.textContent = this.getLocalizedLabelText(
252         "BackgroundImageWizardStepsPanel", "scaleDistanceLabel.text", unitName);
253     component.registerEventListener(component.scaleStep.scaleDistanceInput, "input", function(ev) {
254         controller.setScaleDistance(component.scaleStep.scaleDistanceInput.getValue() != null 
255             ? parseFloat(component.scaleStep.scaleDistanceInput.getValue())
256             : null);
257       });
258     var scaleDistanceChangeListener = function() {
259         var scaleDistance = controller.getScaleDistance();
260         component.scaleStep.scaleDistanceInput.setNullable(scaleDistance === null);
261         component.scaleStep.scaleDistanceInput.setValue(scaleDistance);
262       };
263     scaleDistanceChangeListener();
264     this.registerPropertyChangeListener(controller, "SCALE_DISTANCE", scaleDistanceChangeListener);
265 
266     var zoomInButtonAction = new ResourceAction(preferences, "BackgroundImageWizardStepsPanel", "ZOOM_IN", true);
267     var zoomOutButtonAction = new ResourceAction(preferences, "BackgroundImageWizardStepsPanel", "ZOOM_OUT", true);
268     component.scaleStep.previewZoomIn.style.backgroundImage = "url('" + zoomInButtonAction.getURL(AbstractAction.SMALL_ICON) + "')";
269     component.registerEventListener(component.scaleStep.previewZoomIn, "click", function(ev) {
270         component.scaleStep.preview.width *= 2;
271         component.repaintScaleCanvas();
272       });
273     component.scaleStep.previewZoomOut.style.backgroundImage = "url('" + zoomOutButtonAction.getURL(AbstractAction.SMALL_ICON) + "')";
274     component.registerEventListener(component.scaleStep.previewZoomOut, "click", function(ev) {
275         component.scaleStep.preview.width /= 2;
276         component.repaintScaleCanvas();
277       });
278     this.registerPropertyChangeListener(controller, "SCALE_DISTANCE_POINTS", function() {
279         component.repaintScaleCanvas();
280       });
281 
282     component.repaintScaleCanvas();
283 
284     var canvas = this.scaleStep.preview;
285     canvas.style.touchAction = "none";
286 
287     var mouseUp = function(ev) {
288         if (canvas.dragging) {
289           canvas.dragging = false;
290           canvas.distanceStartPoint = canvas.distanceEndPoint = false;
291         }
292       };
293     var mouseMove = function(ev) {
294         ev.stopImmediatePropagation();
295   
296         var canvasRect = canvas.getBoundingClientRect();
297         var pointerCoordinatesObject = ev.touches && ev.touches.length > 0 ? ev.touches[0] : ev;
298         var x = pointerCoordinatesObject.clientX - canvasRect.left;
299         var y = pointerCoordinatesObject.clientY - canvasRect.top;
300   
301         if (canvas.dragging) {
302           var scale = canvas.width / component.selectedImage.width;
303           var newX = x / scale;
304           var newY = y / scale;
305           var scaleDistancePoints = controller.getScaleDistancePoints();
306           var updatedPoint;
307           var fixedPoint;
308           if (canvas.distanceStartPoint) {
309             updatedPoint = scaleDistancePoints [0];
310             fixedPoint   = scaleDistancePoints [1];
311           } else {
312             updatedPoint = scaleDistancePoints [1];
313             fixedPoint   = scaleDistancePoints [0];
314           }
315           // Accept new points only if distance is greater that 2 pixels
316           if (java.awt.geom.Point2D.distanceSq(fixedPoint [0] * scale, fixedPoint [1] * scale,
317                   newX * scale, newY * scale) >= 4) {
318             // If shift is down constrain keep the line vertical or horizontal
319             if (ev.shiftKey) {
320               var angle = Math.abs(Math.atan2(fixedPoint [1] - newY, newX - fixedPoint [0]));
321               if (angle > Math.PI / 4 && angle <= 3 * Math.PI / 4) {
322                 newX = fixedPoint [0];
323               } else {
324                 newY = fixedPoint [1];
325               }
326             }
327             updatedPoint [0] = newX;
328             updatedPoint [1] = newY;
329             controller.setScaleDistancePoints(
330               scaleDistancePoints[0][0], scaleDistancePoints[0][1],
331               scaleDistancePoints[1][0], scaleDistancePoints[1][1]);
332             component.repaintScaleCanvas();
333           }
334         } else {
335           canvas.distanceStartPoint = 
336           canvas.distanceEndPoint = false;
337           
338           var scaleDistancePoints = controller.getScaleDistancePoints();
339           var scale = canvas.width / component.selectedImage.width;
340           // Check if user clicked on start or end point of distance line
341           if (Math.abs(scaleDistancePoints [0][0] * scale - x) <= CANVAS_TOUCHABLE_AREA_RADIUS
342               && Math.abs(scaleDistancePoints [0][1] * scale - y) <= CANVAS_TOUCHABLE_AREA_RADIUS) {
343             canvas.distanceStartPoint = true;
344           } else if (Math.abs(scaleDistancePoints [1][0] * scale - x) <= CANVAS_TOUCHABLE_AREA_RADIUS
345                      && Math.abs(scaleDistancePoints [1][1] * scale - y) <= CANVAS_TOUCHABLE_AREA_RADIUS) {
346             canvas.distanceEndPoint = true;
347           }
348           
349           if (canvas.distanceStartPoint || canvas.distanceEndPoint) {
350             canvas.style.cursor = "crosshair";
351           } else {
352             canvas.style.cursor = "default";
353           }
354         }
355       };
356     var mouseDown = function(ev) {
357         ev.stopImmediatePropagation();
358         mouseMove(ev);
359   
360         if (canvas.distanceStartPoint || canvas.distanceEndPoint) {
361           canvas.dragging = true;
362         }
363       };
364 
365     this.registerEventListener(canvas, "mousedown", mouseDown, true);
366     this.registerEventListener(canvas, "touchstart", mouseDown, true);
367     this.registerEventListener(canvas, "mousemove", mouseMove, true);
368     this.registerEventListener(canvas, "touchmove", mouseMove, true);
369     this.registerEventListener(canvas, "mouseup", mouseUp, true);
370     this.registerEventListener(canvas, "touchend", mouseUp, true);
371   }
372 
373   /**
374    * @private
375    */
376   BackgroundImageWizardStepsView.prototype.repaintScaleCanvas = function () {
377     var canvas = this.scaleStep.preview;
378     var g2D = new Graphics2D(canvas);
379     g2D.fillRect(0, 0, canvas.width, canvas.height);
380     var image = this.selectedImage;
381     if (image) {
382       canvas.height = (image.height / image.width) * canvas.width;
383       g2D.drawImageWithSize(image, 0, 0, canvas.width, canvas.height);
384       g2D.setColor("blue");
385       var oldTransform = g2D.getTransform();
386       var scale = canvas.width / image.width;
387       g2D.scale(scale, scale);
388       // Draw a scale distance line
389       g2D.setStroke(new java.awt.BasicStroke(5 / scale,
390           java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_BEVEL));
391       var scaleDistancePoints = this.controller.getScaleDistancePoints();
392       g2D.draw(new java.awt.geom.Line2D.Float(scaleDistancePoints [0][0], scaleDistancePoints [0][1],
393                                               scaleDistancePoints [1][0], scaleDistancePoints [1][1]));
394       // Draw start point line
395       g2D.setStroke(new java.awt.BasicStroke(1 / scale,
396           java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_BEVEL));
397       var angle = Math.atan2(scaleDistancePoints [1][1] - scaleDistancePoints [0][1],
398                              scaleDistancePoints [1][0] - scaleDistancePoints [0][0]);
399       var oldTransform2 = g2D.getTransform();
400       g2D.translate(scaleDistancePoints [0][0], scaleDistancePoints [0][1]);
401       g2D.rotate(angle);
402       var endLine = new java.awt.geom.Line2D.Double(0, 5 / scale, 0, -5 / scale);
403       g2D.draw(endLine);
404       g2D.setTransform(oldTransform2);
405 
406       // Draw end point line
407       g2D.translate(scaleDistancePoints [1][0], scaleDistancePoints [1][1]);
408       g2D.rotate(angle);
409       g2D.draw(endLine);
410       g2D.setTransform(oldTransform);
411     }
412   }
413 
414   /**
415    * @private
416    */
417   BackgroundImageWizardStepsView.prototype.initOriginStep = function () {
418     var component = this;
419     var unitName = preferences.getLengthUnit().getName();
420     var maximumLength = preferences.getLengthUnit().getMaximumLength();
421 
422     this.originStep = {
423       panel: this.findElement("[originStep]"),
424       preview: this.findElement("[originStep] [preview] canvas"),
425       previewZoomIn: this.findElement("[originStep] [previewZoomIn]"),
426       previewZoomOut: this.findElement("[originStep] [previewZoomOut]"),
427       xOriginLabel: this.getElement("x-origin-label"),
428       xOriginInput: new JSSpinner(preferences, this.getElement("x-origin-input"), 
429           {
430             format: preferences.getLengthUnit().getFormat(),
431             value: controller.getXOrigin(),
432             minimum: -maximumLength,
433             maximum: maximumLength,
434             stepSize: preferences.getLengthUnit().getStepSize()
435           }),
436       yOriginLabel: this.getElement("y-origin-label"),
437       yOriginInput: new JSSpinner(preferences, this.getElement("y-origin-input"), 
438           {
439             format: preferences.getLengthUnit().getFormat(),
440             value: controller.getYOrigin(),
441             minimum: -maximumLength,
442             maximum: maximumLength,
443             stepSize: preferences.getLengthUnit().getStepSize()
444           }),
445     };
446 
447     this.originStep.xOriginLabel.textContent = this.getLocalizedLabelText(
448         "BackgroundImageWizardStepsPanel", "xOriginLabel.text", unitName);
449     this.originStep.yOriginLabel.textContent = this.getLocalizedLabelText(
450         "BackgroundImageWizardStepsPanel", "yOriginLabel.text", unitName);
451     this.registerEventListener([this.originStep.xOriginInput, this.originStep.yOriginInput], "input", function(ev) {
452         controller.setOrigin(component.originStep.xOriginInput.getValue(), component.originStep.yOriginInput.getValue());
453       });
454     this.registerPropertyChangeListener(controller, "X_ORIGIN", function() {
455         component.originStep.xOriginInput.setValue(controller.getXOrigin());
456         component.repaintOriginCanvas();
457       });
458     this.registerPropertyChangeListener(controller, "Y_ORIGIN", function() {
459         component.originStep.yOriginInput.setValue(controller.getYOrigin());
460         component.repaintOriginCanvas();
461       });
462 
463     var canvas = this.originStep.preview;
464 
465     var zoomInButtonAction = new ResourceAction(preferences, "BackgroundImageWizardStepsPanel", "ZOOM_IN", true);
466     var zoomOutButtonAction = new ResourceAction(preferences, "BackgroundImageWizardStepsPanel", "ZOOM_OUT", true);
467     this.originStep.previewZoomIn.style.backgroundImage = "url('" + zoomInButtonAction.getURL(AbstractAction.SMALL_ICON) + "')";
468     this.registerEventListener(this.originStep.previewZoomIn, "click", function(ev) {
469         component.originStep.preview.width *= 2;
470         component.repaintOriginCanvas();
471       });
472     this.originStep.previewZoomOut.style.backgroundImage = "url('" + zoomOutButtonAction.getURL(AbstractAction.SMALL_ICON) + "')";
473     this.registerEventListener(this.originStep.previewZoomOut, "click", function(ev) {
474         component.originStep.preview.width /= 2;
475         component.repaintOriginCanvas();
476       });
477 
478     var mouseUp = function(ev) {
479         component.isMovingOrigin = false;
480         canvas.style.cursor = "default";
481       };
482     var mouseMove = function(ev) {
483         ev.stopImmediatePropagation();
484         if (component.isMovingOrigin) {
485           var canvasRect = canvas.getBoundingClientRect();
486           var pointerCoordinatesObject = ev.touches && ev.touches.length > 0 ? ev.touches[0] : ev;
487           var scaleDistancePoints = controller.getScaleDistancePoints();
488           var rescale = component.originStep.preview.width / component.selectedImage.width;
489           rescale = rescale / BackgroundImage.getScale(controller.getScaleDistance(),
490               scaleDistancePoints [0][0], scaleDistancePoints [0][1],
491               scaleDistancePoints [1][0], scaleDistancePoints [1][1]);
492           var xOrigin = Math.round((pointerCoordinatesObject.clientX - canvasRect.left) / rescale * 10) / 10;
493           var yOrigin = Math.round((pointerCoordinatesObject.clientY - canvasRect.top) / rescale * 10) / 10;
494           controller.setOrigin(xOrigin, yOrigin);
495           component.repaintOriginCanvas();
496         }
497       };
498     var mouseDown = function(ev) {
499         component.isMovingOrigin = true;
500         canvas.style.cursor = "crosshair";
501         mouseMove(ev);
502       };
503 
504     this.registerEventListener(canvas, "mousedown", mouseDown, true);
505     this.registerEventListener(canvas, "touchstart", mouseDown, true);
506     this.registerEventListener(canvas, "mousemove", mouseMove, true);
507     this.registerEventListener(canvas, "touchmove", mouseMove, true);
508     this.registerEventListener(canvas, "mouseup", mouseUp, true);
509     this.registerEventListener(canvas, "touchend", mouseUp, true);
510   }
511 
512   /**
513    * @private
514    */
515   BackgroundImageWizardStepsView.prototype.repaintOriginCanvas = function () {
516     var canvas = this.originStep.preview;
517     var g2D = new Graphics2D(canvas);
518     g2D.fillRect(0, 0, canvas.width, canvas.height);
519     var image = this.selectedImage;
520     if (image) {
521       canvas.height = (image.height / image.width) * canvas.width;
522       g2D.drawImageWithSize(image, 0, 0, canvas.width, canvas.height);
523       g2D.setColor("blue");
524       var oldTransform = g2D.getTransform();
525       var scale = canvas.width / image.width;
526       var scaleDistancePoints = this.controller.getScaleDistancePoints();
527       scale = scale / BackgroundImage.getScale(this.controller.getScaleDistance(),
528           scaleDistancePoints [0][0], scaleDistancePoints [0][1],
529           scaleDistancePoints [1][0], scaleDistancePoints [1][1]);
530       g2D.scale(scale, scale);
531 
532       // Draw a dot at origin
533       g2D.translate(this.controller.getXOrigin(), this.controller.getYOrigin());
534       
535       var originRadius = 4 / scale;
536       g2D.fill(new java.awt.geom.Ellipse2D.Float(-originRadius, -originRadius,
537           originRadius * 2, originRadius * 2));
538 
539       // Draw a cross
540       g2D.setStroke(new java.awt.BasicStroke(1 / scale,
541           java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_BEVEL));
542       g2D.draw(new java.awt.geom.Line2D.Double(8 / scale, 0, -8 / scale, 0));
543       g2D.draw(new java.awt.geom.Line2D.Double(0, 8 / scale, 0, -8 / scale));
544       g2D.setTransform(oldTransform);
545     }
546   }
547 
548   /**
549    * Sets the texts of label and button of image choice panel with change texts.
550    * @private
551    */
552   BackgroundImageWizardStepsView.prototype.setImageChangeTexts = function () {
553     this.imageChoiceStep.imageChoiceOrChangeLabel.innerHTML = this.getLocalizedLabelText(
554         "BackgroundImageWizardStepsPanel", "imageChangeLabel.text").replace(/\<br\>/g, " ");
555     this.imageChoiceStep.imageChoiceOrChangeButton.innerHTML = this.getLocalizedLabelText(
556         "BackgroundImageWizardStepsPanel", "imageChangeButton.text");
557   }
558 
559   /**
560    * Sets the texts of label and button of image choice panel with choice texts.
561    * @private
562    */
563   BackgroundImageWizardStepsView.prototype.setImageChoiceTexts = function () {
564     this.imageChoiceStep.imageChoiceOrChangeLabel.innerHTML = this.getLocalizedLabelText(
565         "BackgroundImageWizardStepsPanel", "imageChoiceLabel.text").replace(/\<br\>/g, " ");
566     this.imageChoiceStep.imageChoiceOrChangeButton.innerHTML = this.getLocalizedLabelText(
567         "BackgroundImageWizardStepsPanel", "imageChoiceButton.text");
568   }
569 
570   /**
571    * @param {BackgroundImage} backgroundImage
572    * @private
573    */
574   BackgroundImageWizardStepsView.prototype.updateImage = function(backgroundImage) {
575     if (backgroundImage == null) {
576       this.setImageChoiceTexts();
577       this.updatePreviewComponentsImage();
578     } else {
579       this.setImageChangeTexts();
580 
581       // In Java's version: BackgroundImageWizardStepsPanel, image is updated in EDT (using invokeLater) when wizard view is initialized
582       // here, if we setImage right away, wizard won't be initialized yet, and next state enabled value won't be refreshed properly
583       // with this setTimeout, we ensure this code runs in next event loop
584       setTimeout(function() {
585           controller.setImage(backgroundImage.getImage());
586           controller.setScaleDistance(backgroundImage.getScaleDistance());
587           controller.setScaleDistancePoints(backgroundImage.getScaleDistanceXStart(),
588               backgroundImage.getScaleDistanceYStart(), backgroundImage.getScaleDistanceXEnd(),
589               backgroundImage.getScaleDistanceYEnd());
590           controller.setOrigin(backgroundImage.getXOrigin(), backgroundImage.getYOrigin());
591         }, 100);
592     }
593   }
594 
595   /**
596    * @param {HTMLImageElement?} image
597    * @param {File} file
598    * @private
599    */
600   BackgroundImageWizardStepsView.prototype.updateController = function(image, file) {
601     var view = this;
602     var controller = this.controller;
603     var imageType = ImageTools.isImageWithAlpha(image) ? "image/png" : "image/jpeg";
604     this.checkImageSize(image, imageType, function(checkedImage) {
605         var contentReady = function(content) {
606             setTimeout(function() {
607                 controller.setImage(content);
608                 view.setImageChangeTexts();
609                 var referenceBackgroundImage = controller.getReferenceBackgroundImage();
610                 if (referenceBackgroundImage != null
611                     && referenceBackgroundImage.getScaleDistanceXStart() < checkedImage.width
612                     && referenceBackgroundImage.getScaleDistanceXEnd() < checkedImage.width
613                     && referenceBackgroundImage.getScaleDistanceYStart() < checkedImage.height
614                     && referenceBackgroundImage.getScaleDistanceYEnd() < checkedImage.height) {
615                   // Initialize distance and origin with values of the reference checkedImage
616                   controller.setScaleDistance(referenceBackgroundImage.getScaleDistance());
617                   controller.setScaleDistancePoints(referenceBackgroundImage.getScaleDistanceXStart(),
618                       referenceBackgroundImage.getScaleDistanceYStart(),
619                       referenceBackgroundImage.getScaleDistanceXEnd(),
620                       referenceBackgroundImage.getScaleDistanceYEnd());
621                   controller.setOrigin(referenceBackgroundImage.getXOrigin(), referenceBackgroundImage.getYOrigin());
622                 } else {
623                   // Initialize distance and origin with default values
624                   controller.setScaleDistance(null);
625                   var scaleDistanceXStart = checkedImage.width * 0.1;
626                   var scaleDistanceYStart = checkedImage.height / 2;
627                   var scaleDistanceXEnd = checkedImage.width * 0.9;
628                   controller.setScaleDistancePoints(scaleDistanceXStart, scaleDistanceYStart,
629                       scaleDistanceXEnd, scaleDistanceYStart);
630                   controller.setOrigin(0, 0);
631                 }
632               }, 100);
633           };
634         if (image === checkedImage
635             && (file.type == "image/jpeg" 
636                || file.type == "image/png")) {
637           contentReady(BlobURLContent.fromBlob(file));
638         } else {
639           BlobURLContent.fromImage(checkedImage, imageType, contentReady);
640         }
641       });
642   }
643 
644   /**
645    * @param {HTMLImageElement} image
646    * @param {string} imageType can be "image/png" or "image/jpeg" depending on image alpha channel requirements
647    * @param {function(HTMLImageElement)} imageReady function called after resize with resized image (or with original image if resize was not necessary or declined by user)
648    * @private
649    */
650   BackgroundImageWizardStepsView.prototype.checkImageSize = function (image, imageType, imageReady) {
651     if (image.width * image.height < LARGE_IMAGE_PIXEL_COUNT_THRESHOLD) {
652       imageReady(image);
653     } else {
654       var stepsView = this;
655       var factor = Math.sqrt(LARGE_IMAGE_MAX_PIXEL_COUNT / (image.width * image.height));
656       var reducedWidth = Math.round(image.width * factor);
657       var reducedHeight = Math.round(image.height * factor);
658       var resizeImage = function() { 
659           ImageTools.resize(image, reducedWidth, reducedHeight, imageReady, imageType);
660         };
661       if (this.getUserPreferences().isImportedImageResizedWithoutPrompting()) {
662         resizeImage();
663       } else {
664         var promptDialog = new JSImageResizingDialog(preferences,
665             "@{BackgroundImageWizardStepsPanel.reduceImageSize.title}",
666             stepsView.getLocalizedLabelText(
667                 "BackgroundImageWizardStepsPanel", "reduceImageSize.message", [image.width, image.height, reducedWidth, reducedHeight]),
668             "@{BackgroundImageWizardStepsPanel.reduceImageSize.cancel}",
669             "@{BackgroundImageWizardStepsPanel.reduceImageSize.keepUnchanged}",
670             "@{BackgroundImageWizardStepsPanel.reduceImageSize.reduceSize}",
671             resizeImage, // Confirm image resizing
672             function() { // Original image 
673               imageReady(image); 
674             });
675         promptDialog.displayView();
676       }
677     }
678   }
679 
680   /**
681    * @private
682    */
683   BackgroundImageWizardStepsView.prototype.updatePreviewComponentsImage = function() {
684     var component = this;
685     var image = this.controller.getImage();
686 
687     delete this.imageChoiceStep.preview.src;
688     this.imageChoiceStep.preview.width = 0;
689     delete this.scaleStep.preview.src;
690     delete this.originStep.preview.src;
691     if (image != null) {
692       TextureManager.getInstance().loadTexture(image, {
693           textureUpdated: function(image) {
694             component.imageChoiceStep.preview.src = image.src;
695             component.imageChoiceStep.preview.width = image.width;
696             component.selectedImage = image;
697             if (image.width > 400) {
698               component.scaleStep.preview.width = 400;
699               component.originStep.preview.width = 400;
700             }
701     
702             component.repaintScaleCanvas();
703             component.repaintOriginCanvas();
704           },
705           textureError: function(error) {
706             imageErrorListener(error);
707           }
708         });
709     }
710   }
711 
712   /**
713    * Changes displayed view based on current step.
714    */
715   BackgroundImageWizardStepsView.prototype.updateStep = function() {
716     var step = this.controller.getStep();
717     switch (step) {
718       case BackgroundImageWizardController.Step.CHOICE:
719         this.imageChoiceStep.panel.style.display = "flex";
720         this.scaleStep.panel.style.display = "none";
721         this.originStep.panel.style.display = "none";
722         break;
723       case BackgroundImageWizardController.Step.SCALE:
724         this.imageChoiceStep.panel.style.display = "none";
725         this.scaleStep.panel.style.display = "flex";
726         delete this.scaleStep.panel.style.display;
727         this.originStep.panel.style.display = "none";
728         break;
729       case BackgroundImageWizardController.Step.ORIGIN:
730         this.imageChoiceStep.panel.style.display = "none";
731         this.scaleStep.panel.style.display = "none";
732         this.originStep.panel.style.display = "flex";
733         break;
734     }
735   };
736 
737   return new BackgroundImageWizardStepsView();
738 }
739 
740 JSViewFactory.prototype.createImportedFurnitureWizardStepsView = function(piece, modelName, importHomePiece, preferences, importedFurnitureWizardController) {
741   return null;
742 }
743 
744 /**
745  * @param {CatalogTexture} texture 
746  * @param {string} textureName 
747  * @param {UserPreferences} preferences 
748  * @param {ImportedTextureWizardController} controller 
749  * @return {JSComponent}
750  */
751 JSViewFactory.prototype.createImportedTextureWizardStepsView = function(texture, textureName, preferences, controller) {
752   var LARGE_IMAGE_PIXEL_COUNT_THRESHOLD = 640 * 640;
753   var IMAGE_PREFERRED_MAX_SIZE = 512;
754   var LARGE_IMAGE_MAX_PIXEL_COUNT = IMAGE_PREFERRED_MAX_SIZE * IMAGE_PREFERRED_MAX_SIZE;
755 
756   function ImportedTextureWizardStepsView() {
757     JSComponent.call(this, preferences,
758           "<div imageStep>" 
759         + "  <div description>@{ImportedTextureWizardStepsPanel.imageChangeLabel.text}</div>"
760         + "  <div class='buttons'>"
761         + "    <button changeImage>@{ImportedTextureWizardStepsPanel.imageChangeButton.text}</button>"
762         + "    <button findImage>@{ImportedTextureWizardStepsPanel.findImagesButton.text}</button>"
763         + "    <input type='file' accept='image/*' style='display: none'/>"
764         + "  </div>"
765         + "  <div preview>"
766         + "    <img>"
767         + "  </div>"
768         + "</div>"
769         + "<div attributesStep>"
770         + "  <div description></div>"
771         + "  <div form>"
772         + "    <div preview>"
773         + "      <img />"
774         + "    </div>"
775         + "    <div>@{ImportedTextureWizardStepsPanel.nameLabel.text}</div>"
776         + "    <div>"
777         + "      <input type='text' name='name' />"
778         + "    </div>"
779         + "    <div>@{ImportedTextureWizardStepsPanel.categoryLabel.text}</div>"
780         + "    <div>"
781         + "      <select name='category'></select>"
782         + "    </div>"
783         + "    <div>@{ImportedTextureWizardStepsPanel.creatorLabel.text}</div>"
784         + "    <div>"
785         + "      <input type='text' name='creator' />"
786         + "    </div>"
787         + "    <div data-name='width-label' class='label-cell'></div>"
788         + "    <div>"
789         + "      <span data-name='width-input'></span>"
790         + "    </div>"
791         + "    <div data-name='height-label' class='label-cell'></div>"
792         + "    <div>"
793         + "      <span data-name='height-input'></span>"
794         + "    </div>"
795         + "  </div>"
796         + "</div>");
797 
798     this.controller = controller;
799     this.userCategory = new TexturesCategory(
800         ResourceAction.getLocalizedLabelText(preferences, "ImportedTextureWizardStepsPanel", "userCategory"));
801     this.getHTMLElement().classList.add("imported-texture-wizard");
802     
803     this.initComponents();
804 
805     var component = this;
806     this.registerPropertyChangeListener(controller, "STEP", function(ev) {
807         component.updateStep();
808       });
809   }
810   ImportedTextureWizardStepsView.prototype = Object.create(JSComponent.prototype);
811   ImportedTextureWizardStepsView.prototype.constructor = ImportedTextureWizardStepsView;
812 
813   /**
814    * @private
815    */
816   ImportedTextureWizardStepsView.prototype.initComponents = function () {
817     this.imageStepPanel = this.findElement("[imageStep]");
818     this.imageChoiceOrChangeLabel = this.findElement("[imageStep] [description]");
819     this.imageChoiceOrChangeButton = this.findElement("button[changeImage]");
820     this.imageFindImageButton = this.findElement("button[findImage]");
821     this.imageChooserInput = this.findElement("input[type='file']");
822     this.previewPanel = this.findElement("[preview]");
823   
824     if (texture == null) {
825       this.setImageChoiceTexts();
826     }
827   
828     this.attributesStepPanel = this.findElement("[attributesStep]");
829     this.attributesStepPanelDescription = this.findElement("[attributesStep] [description]");
830     
831     this.attributesPreviewPanel = this.findElement("[attributesStep] [preview]");
832     
833     this.nameInput = this.findElement("input[name='name']");
834     this.categorySelect = this.findElement("select[name='category']");
835     this.creatorInput = this.findElement("input[name='creator']");
836   
837     var unitName = preferences.getLengthUnit().getName();
838     var minimumLength = preferences.getLengthUnit().getMinimumLength();
839     var maximumLength = preferences.getLengthUnit().getMaximumLength();
840     this.widthLabel = this.getElement("width-label"),
841     this.widthLabel.textContent = this.getLocalizedLabelText(
842         "ImportedTextureWizardStepsPanel", "widthLabel.text", unitName);
843     this.widthInput = new JSSpinner(preferences, this.getElement("width-input"), 
844         {
845           format: preferences.getLengthUnit().getFormat(),
846           minimum: minimumLength,
847           maximum: maximumLength,
848           stepSize: preferences.getLengthUnit().getStepSize()
849         });
850     this.heightLabel = this.getElement("height-label"),
851     this.heightLabel.textContent = this.getLocalizedLabelText(
852         "ImportedTextureWizardStepsPanel", "heightLabel.text", unitName);
853     this.heightInput = new JSSpinner(preferences, this.getElement("height-input"), 
854         {
855           format: preferences.getLengthUnit().getFormat(),
856           minimum: minimumLength,
857           maximum: maximumLength,
858           stepSize: preferences.getLengthUnit().getStepSize()
859         });
860     
861     var component = this;
862     var imageErrorListener = function(ev) {
863         console.warn("Error loading image: " + ev);
864         component.controller.setImage(null);
865         component.setImageChoiceTexts();
866         component.updatePreviewComponentsImage();
867         alert(ResourceAction.getLocalizedLabelText(preferences, "ImportedTextureWizardStepsPanel",
868             "imageChoiceErrorLabel.text"));
869       };
870     this.registerEventListener(this.imageChoiceOrChangeButton, "click", function(ev) {
871         component.imageChooserInput.click();
872       });  
873     this.registerEventListener(this.imageFindImageButton, "click", function(ev) {
874         try {
875           var url = preferences.getLocalizedString("ImportedTextureWizardStepsPanel", "findImagesButton.url");
876           window.open(url, "_blank");
877         } catch (e) {
878           this.imageFindImageButton.style.display = "none";
879         }
880       });  
881     var importImage = function(file) {
882         if (file) {
883           var reader = new FileReader();
884           // Use onload and onerror rather that addEventListener for Cordova support
885           reader.onload = function(ev) {
886               var image = new Image();
887               image.addEventListener("load", function(ev) {
888                 component.updateController(image, file);
889               });
890               image.addEventListener("error", imageErrorListener);
891               image.src = ev.target.result;
892             };
893           reader.onerror = imageErrorListener;
894           reader.readAsDataURL(file);
895         }
896       };
897     this.registerEventListener(this.imageChooserInput, "input", function(ev) {
898         importImage(this.files[0]);
899       });
900     this.registerEventListener(this.previewPanel, "drop", function(ev) {
901         ev.preventDefault();
902         importImage(ev.dataTransfer.files[0]);
903       });
904     this.registerEventListener(this.previewPanel, "dragover", function(ev) {
905         ev.preventDefault();
906       });
907   
908     this.registerPropertyChangeListener(controller, "IMAGE", function(ev) {
909         component.updatePreviewComponentsImage();
910       });
911     this.registerPropertyChangeListener(controller, "WIDTH", function(ev) {
912         component.updatePreviewComponentsImage();
913       });
914     this.registerPropertyChangeListener(controller, "HEIGHT", function(ev) {
915         component.updatePreviewComponentsImage();
916       });
917   
918     var categories = preferences.getTexturesCatalog().getCategories();
919     if (this.findUserCategory(categories) == null) {
920       categories = categories.concat([this.userCategory]);
921     }
922     for (var i = 0; i < categories.length; i++) {
923       var option = document.createElement("option");
924       option.value = categories[i].getName();
925       option.textContent = categories[i].getName();
926       option._category = categories[i];
927       this.categorySelect.appendChild(option);
928     }
929   
930     this.attributesStepPanelDescription.innerHTML = this.getLocalizedLabelText(
931         "ImportedTextureWizardStepsPanel", "attributesLabel.text").replace(/\<br\>/g, " ");
932     this.registerPropertyChangeListener(controller, "NAME", function() {
933         if (component.nameInput.value.trim() != controller.getName()) {
934           component.nameInput.value = controller.getName();
935         }
936       });
937     this.registerEventListener(this.nameInput, "input", function(ev) {
938         controller.setName(component.nameInput.value.trim());
939       });
940   
941     this.registerPropertyChangeListener(controller, "CATEGORY", function(ev) {
942         var category = controller.getCategory();
943         if (category != null) {
944           component.categorySelect.value = category.getName();
945         }
946       });
947     this.registerEventListener(this.categorySelect, "change", function(ev) {
948         var category = component.categorySelect.item(component.categorySelect.selectedIndex)._category;
949         controller.setCategory(category);
950       });
951   
952     this.registerPropertyChangeListener(controller, "CREATOR", function(ev) {
953         if (component.creatorInput.value.trim() != controller.getCreator()) {
954           component.creatorInput.value = controller.getCreator();
955         }
956       });
957     this.registerEventListener(component.creatorInput, "input", function(ev) {
958         controller.setCreator(component.creatorInput.value.trim());
959       });
960   
961     this.registerPropertyChangeListener(controller, "WIDTH", function(ev) {
962         component.widthInput.setValue(controller.getWidth());
963       });
964     this.registerEventListener(this.widthInput, "input", function(ev) {
965         controller.setWidth(parseFloat(component.widthInput.value));
966       });
967   
968     this.registerPropertyChangeListener(controller, "HEIGHT", function(ev) {
969         component.heightInput.setValue(controller.getHeight());
970       });
971     this.registerEventListener(this.heightInput, "input", function(ev) {
972         controller.setHeight(parseFloat(component.heightInput.value));
973       });
974   
975     if (texture != null) {
976       TextureManager.getInstance().loadTexture(texture.getImage(), 
977           {
978             textureUpdated: function(image) {
979               component.updateController(image, texture);
980             },
981             textureError: function(error) {
982               imageErrorListener(error);
983             }
984           });
985     }
986   }
987 
988   /**
989    * @param {HTMLImageElement?} image 
990    * @param {File|CatalogTexture} file
991    * @private
992    */
993   ImportedTextureWizardStepsView.prototype.updateController = function(image, file) {
994     var component = this;
995     var controller = this.controller;
996     this.setImageChangeTexts();
997     if (file instanceof CatalogTexture) {
998       var catalogTexture = file;
999       setTimeout(function() {
1000           controller.setImage(catalogTexture.getImage());
1001           controller.setName(catalogTexture.getName());
1002           controller.setCategory(catalogTexture.getCategory());
1003           controller.setCreator(catalogTexture.getCreator());
1004           controller.setWidth(catalogTexture.getWidth());
1005           controller.setHeight(catalogTexture.getHeight());
1006         }, 100);
1007     } else { 
1008       // File
1009       var textureName = "Texture";
1010       if (file.name.lastIndexOf('.') > 0) {
1011         var parts = file.name.split(/\/|\\|\./);
1012         if (parts.length > 1) {
1013           textureName = parts [parts.length - 2];
1014         }
1015       }
1016       var imageType = ImageTools.isImageWithAlpha(image) ? "image/png" : "image/jpeg";
1017       this.checkImageSize(image, imageType, function(checkedImage) {
1018           var contentReady = function(content) {
1019               setTimeout(function() {
1020                   controller.setImage(content);
1021                   controller.setName(textureName);
1022                   var categories = component.preferences.getTexturesCatalog().getCategories();
1023                   var userCategory = component.findUserCategory(categories) || component.userCategory;
1024                   controller.setCategory(userCategory);
1025                   controller.setCreator(null);
1026                   var defaultWidth = component.preferences.getLengthUnit().isMetric() 
1027                       ? 20 : LengthUnit.inchToCentimeter(8);
1028                   controller.setWidth(defaultWidth);
1029                   controller.setHeight(defaultWidth / checkedImage.width * checkedImage.height);
1030                 }, 100);
1031             };
1032           if (image === checkedImage
1033               && (file.type == "image/jpeg" 
1034                 || file.type == "image/png")) {
1035             contentReady(BlobURLContent.fromBlob(file));
1036           } else {
1037             BlobURLContent.fromImage(checkedImage, imageType, contentReady);
1038           }
1039         });
1040     }
1041   }
1042 
1043   /**
1044    * Sets the texts of label and button of image choice panel with change texts.
1045    * @private
1046    */
1047   ImportedTextureWizardStepsView.prototype.setImageChangeTexts = function() {
1048     this.imageChoiceOrChangeLabel.innerHTML = this.getLocalizedLabelText(
1049         "ImportedTextureWizardStepsPanel", "imageChangeLabel.text").replace(/\<br\>/g, " ");
1050     this.imageChoiceOrChangeButton.innerHTML = this.getLocalizedLabelText(
1051         "ImportedTextureWizardStepsPanel", "imageChangeButton.text");
1052   }
1053 
1054   /**
1055    * Sets the texts of label and button of image choice panel with choice texts.
1056    * @private
1057    */
1058   ImportedTextureWizardStepsView.prototype.setImageChoiceTexts = function() {
1059     this.imageChoiceOrChangeLabel.innerHTML = this.getLocalizedLabelText(
1060         "ImportedTextureWizardStepsPanel", "imageChoiceLabel.text").replace(/\<br\>/g, " ");
1061     this.imageChoiceOrChangeButton.innerHTML = this.getLocalizedLabelText(
1062         "ImportedTextureWizardStepsPanel", "imageChoiceButton.text");
1063   }
1064 
1065   /**
1066    * Returns user category if it exists among existing the given <code>categories</code>.
1067    * @param {TexturesCategory[]} categories 
1068    * @return {TexturesCategory | null} found user category, or null if not found
1069    * @private
1070    */
1071   ImportedTextureWizardStepsView.prototype.findUserCategory = function(categories) {
1072     var categories = preferences.getTexturesCatalog().getCategories();
1073     for (var i = 0; i < categories.length; i++) {
1074       if (categories[i].equals(this.userCategory)) {
1075         return categories[i];
1076       }
1077     }
1078     return null;
1079   }
1080 
1081   /**
1082    * @param {HTMLImageElement} image 
1083    * @param {string} imageType can be "image/png" or "image/jpeg" depending on image alpha channel requirements
1084    * @param {function(HTMLImageElement)} imageReady function called after resize with resized image (or with original image if resize was not necessary or declined by user)
1085    * @private
1086    */
1087   ImportedTextureWizardStepsView.prototype.checkImageSize = function (image, imageType, imageReady) {
1088     if (image.width * image.height < LARGE_IMAGE_PIXEL_COUNT_THRESHOLD) {
1089       imageReady(image);
1090     } else {
1091       var factor;
1092       var ratio = image.width / image.height;
1093       if (ratio < 0.5 || ratio > 2) {
1094         factor = Math.sqrt(LARGE_IMAGE_MAX_PIXEL_COUNT / (image.width * image.height));
1095       } else if (ratio < 1) {
1096         factor = IMAGE_PREFERRED_MAX_SIZE / image.height;
1097       } else {
1098         factor = IMAGE_PREFERRED_MAX_SIZE / image.width;
1099       }
1100     
1101       var reducedWidth = Math.round(image.width * factor);
1102       var reducedHeight = Math.round(image.height * factor);
1103       var resizeImage = function() { 
1104           ImageTools.resize(image, reducedWidth, reducedHeight, imageReady, imageType);
1105         };
1106       if (this.getUserPreferences().isImportedImageResizedWithoutPrompting()) {
1107         resizeImage();
1108       } else {
1109         var promptDialog = new JSImageResizingDialog(preferences,
1110             "@{ImportedTextureWizardStepsPanel.reduceImageSize.title}",
1111             this.getLocalizedLabelText(
1112                 "ImportedTextureWizardStepsPanel", "reduceImageSize.message", [image.width, image.height, reducedWidth, reducedHeight]),
1113             "@{ImportedTextureWizardStepsPanel.reduceImageSize.cancel}",
1114             "@{ImportedTextureWizardStepsPanel.reduceImageSize.keepUnchanged}",
1115             "@{ImportedTextureWizardStepsPanel.reduceImageSize.reduceSize}",
1116             resizeImage, // Confirm image resizing
1117             function() { // Original image 
1118                 imageReady(image); 
1119               });
1120         promptDialog.displayView();
1121       }
1122     }
1123   }
1124 
1125   /**
1126    * @private
1127    */
1128   ImportedTextureWizardStepsView.prototype.updatePreviewComponentsImage = function() {
1129     this.previewPanel.innerHTML = "";
1130     var image = new Image();
1131     if (this.controller.getImage() !== null) {
1132       this.controller.getImage().getStreamURL({
1133           urlReady: function(url) {
1134             image.src = url;   
1135           }
1136         });
1137     }
1138     this.previewPanel.appendChild(image);
1139     
1140     this.attributesPreviewPanel.innerHTML = "";
1141     var previewImage = document.createElement("div");
1142     previewImage.style.backgroundImage = "url('" + image.src + "')";
1143     previewImage.style.backgroundRepeat = "repeat";
1144 
1145     var widthFactor = this.controller.getWidth() / 250;
1146     var heightFactor = this.controller.getHeight() / 250;
1147     previewImage.style.backgroundSize = "calc(100% * " + widthFactor + ") calc(100% * " + heightFactor + ")";  
1148     previewImage.classList.add("image");
1149     this.attributesPreviewPanel.appendChild(previewImage);
1150   }
1151 
1152   /**
1153    * Changes displayed view based on current step.
1154    */
1155   ImportedTextureWizardStepsView.prototype.updateStep = function() {
1156     var step = this.controller.getStep();
1157     switch (step) {
1158       case ImportedTextureWizardController.Step.IMAGE:
1159         this.imageStepPanel.style.display = "block";
1160         this.attributesStepPanel.style.display = "none";
1161         break;
1162       case ImportedTextureWizardController.Step.ATTRIBUTES:
1163         this.imageStepPanel.style.display = "none";
1164         this.attributesStepPanel.style.display = "block";
1165         break;
1166     }
1167   }
1168 
1169   return new ImportedTextureWizardStepsView();
1170 }
1171 
1172 /**
1173  * @param {UserPreferences} preferences 
1174  * @param {UserPreferencesController} controller 
1175  */
1176 JSViewFactory.prototype.createUserPreferencesView = function(preferences, controller) {
1177   /**
1178    * @param {HTMLElement} element 
1179    * @return {boolean} true if element is displayed (not hidden by css rule)
1180    * @private
1181    */
1182   var isElementVisible = function(element) {
1183     if (element instanceof JSComponent) {
1184       element = element.getHTMLElement();
1185     }
1186     return window.getComputedStyle(element).display !== "none";
1187   }
1188 
1189   /**
1190    * Hides a preference row from any of its input element.
1191    * @param {HTMLElement} preferenceInput 
1192    */
1193   var disablePreferenceRow = function(preferenceInput) {
1194     preferenceInput.parentElement.style.display = "none";
1195 
1196     // Search root input cell
1197     var currentElement = preferenceInput;
1198     while (currentElement.parentElement != null && !currentElement.parentElement.classList.contains("user-preferences-dialog")) {
1199       currentElement = currentElement.parentElement;
1200     }
1201 
1202     // Hide input cell and its sibling label cell
1203     currentElement.style.display = "none";
1204     currentElement.previousElementSibling.style.display = "none";
1205   }
1206   
1207   var dialog = new JSDialog(preferences, 
1208       "@{UserPreferencesPanel.preferences.title}", 
1209       document.getElementById("user-preferences-dialog-template"), 
1210       {
1211         applier: function(dialog) {
1212           if (isElementVisible(dialog.languageSelect)) {
1213             var selectedLanguageOption = dialog.languageSelect.options[dialog.languageSelect.selectedIndex];
1214             controller.setLanguage(selectedLanguageOption != null ? selectedLanguageOption.value : null);
1215           }
1216           if (isElementVisible(dialog.furnitureCatalogViewTreeRadioButton)) {
1217             controller.setFurnitureCatalogViewedInTree(dialog.furnitureCatalogViewTreeRadioButton.checked);
1218           }
1219           if (isElementVisible(dialog.navigationPanelCheckBox)) {
1220             controller.setNavigationPanelVisible(dialog.navigationPanelCheckBox.checked);
1221           }
1222           if (isElementVisible(dialog.editingIn3DViewCheckBox)) {
1223             controller.setEditingIn3DViewEnabled(dialog.editingIn3DViewCheckBox.checked);
1224           }
1225           if (isElementVisible(dialog.aerialViewCenteredOnSelectionCheckBox)) {
1226             controller.setAerialViewCenteredOnSelectionEnabled(dialog.aerialViewCenteredOnSelectionCheckBox.checked);
1227           }
1228           if (isElementVisible(dialog.observerCameraSelectedAtChangeCheckBox)) {
1229             controller.setObserverCameraSelectedAtChange(dialog.observerCameraSelectedAtChangeCheckBox.checked);
1230           }
1231           if (isElementVisible(dialog.magnetismCheckBox)) {
1232             controller.setMagnetismEnabled(dialog.magnetismCheckBox.checked);
1233           }
1234           if (isElementVisible(dialog.rulersCheckBox)) {
1235             controller.setRulersVisible(dialog.rulersCheckBox.checked);
1236           }
1237           if (isElementVisible(dialog.gridCheckBox)) {
1238             controller.setGridVisible(dialog.gridCheckBox.checked);
1239           }
1240           if (isElementVisible(dialog.iconTopViewRadioButton)) {
1241             controller.setFurnitureViewedFromTop(dialog.iconTopViewRadioButton.checked);
1242           }
1243           if (isElementVisible(dialog.iconSizeSelect) && !dialog.iconSizeSelect.disabled) {
1244             controller.setFurnitureModelIconSize(parseInt(dialog.iconSizeSelect.value));
1245           }
1246           if (isElementVisible(dialog.roomRenderingFloorColorOrTextureRadioButton)) {
1247             controller.setRoomFloorColoredOrTextured(dialog.roomRenderingFloorColorOrTextureRadioButton.checked);
1248           }
1249           if (isElementVisible(dialog.newWallThicknessInput)) {
1250             controller.setNewWallThickness(parseFloat(dialog.newWallThicknessInput.getValue()));
1251           }
1252           if (isElementVisible(dialog.newWallHeightInput)) {
1253             controller.setNewWallHeight(parseFloat(dialog.newWallHeightInput.getValue()));
1254           }
1255           if (isElementVisible(dialog.newFloorThicknessInput)) {
1256             controller.setNewFloorThickness(parseFloat(dialog.newFloorThicknessInput.getValue()));
1257           }
1258           controller.modifyUserPreferences();
1259         }
1260       });
1261   
1262 
1263   // LANGUAGE
1264   dialog.languageSelect = dialog.getElement("language-select");
1265   if (controller.isPropertyEditable("LANGUAGE")) {
1266     var supportedLanguages = preferences.getSupportedLanguages();
1267     for (var i = 0; i < supportedLanguages.length; i++) {
1268       var languageCode = supportedLanguages[i].replace('_', '-');
1269       var languageDisplayName = languageCode;
1270       try {
1271         languageDisplayName = new Intl.DisplayNames([languageCode, "en"], { type: "language" }).of(languageCode);
1272         if (languageDisplayName == languageCode) {
1273           throw "No support for Intl.DisplayNames";
1274         }
1275         languageDisplayName = languageDisplayName.charAt(0).toUpperCase() + languageDisplayName.slice(1);
1276       } catch (ex) {
1277         languageDisplayName = {"bg": "Български",
1278                                "cs": "Čeština",
1279                                "de": "Deutsch",
1280                                "el": "Ελληνικά",
1281                                "en": "English",
1282                                "es": "Español",
1283                                "fr": "Français",
1284                                "it": "Italiano",
1285                                "ja": "日本語",
1286                                "hu": "Magyar",
1287                                "nl": "Nederlands",
1288                                "pl": "Polski",
1289                                "pt": "Português",
1290                                "pt-BR": "Português (Brasil)",
1291                                "ru": "Русский",
1292                                "sv": "Svenska",
1293                                "vi": "Tiếng Việt",
1294                                "zh-CN": "中文(中国)",
1295                                "zh-TW": "中文(台灣)"} [languageCode];
1296         if (languageDisplayName === undefined) {
1297           languageDisplayName = languageCode;
1298           console.log("Unknown display name for " + languageCode);
1299         }
1300       }
1301 
1302       var selected = supportedLanguages[i] == controller.getLanguage();
1303       var languageOption = JSComponent.createOptionElement(supportedLanguages[i], languageDisplayName, selected);
1304       dialog.languageSelect.appendChild(languageOption);
1305     }
1306   } else {
1307     disablePreferenceRow(dialog.languageSelect);
1308   }
1309 
1310   // UNIT
1311   dialog.unitSelect = dialog.getElement("unit-select");
1312   if (controller.isPropertyEditable("UNIT")) {
1313     dialog.unitSelect.appendChild(
1314         JSComponent.createOptionElement("MILLIMETER", 
1315             preferences.getLocalizedString("UserPreferencesPanel", "unitComboBox.millimeter.text"),
1316             controller.getUnit() == LengthUnit.MILLIMETER));
1317     dialog.unitSelect.appendChild(
1318         JSComponent.createOptionElement("CENTIMETER", 
1319             preferences.getLocalizedString("UserPreferencesPanel", "unitComboBox.centimeter.text"),
1320             controller.getUnit() == LengthUnit.CENTIMETER));
1321     dialog.unitSelect.appendChild(
1322         JSComponent.createOptionElement("METER", 
1323             preferences.getLocalizedString("UserPreferencesPanel", "unitComboBox.meter.text"),
1324             controller.getUnit() == LengthUnit.METER));
1325     dialog.unitSelect.appendChild(
1326         JSComponent.createOptionElement("INCH", 
1327             preferences.getLocalizedString("UserPreferencesPanel", "unitComboBox.inch.text"),
1328             controller.getUnit() == LengthUnit.INCH));
1329     dialog.unitSelect.appendChild(
1330         JSComponent.createOptionElement("INCH_FRACTION", 
1331             preferences.getLocalizedString("UserPreferencesPanel", "unitComboBox.inchFraction.text"),
1332             controller.getUnit() == LengthUnit.INCH_FRACTION));
1333     dialog.unitSelect.appendChild(
1334         JSComponent.createOptionElement("INCH_DECIMALS", 
1335             preferences.getLocalizedString("UserPreferencesPanel", "unitComboBox.inchDecimals.text"),
1336             controller.getUnit() == LengthUnit.INCH_DECIMALS));
1337     dialog.unitSelect.appendChild(
1338         JSComponent.createOptionElement("FOOT_DECIMALS", 
1339             preferences.getLocalizedString("UserPreferencesPanel", "unitComboBox.footDecimals.text"),
1340             controller.getUnit() == LengthUnit.FOOT_DECIMALS));
1341 
1342     dialog.registerEventListener(dialog.unitSelect, "change", function(ev) {
1343         var selectedUnitOption = dialog.unitSelect.options[dialog.unitSelect.selectedIndex];
1344         controller.setUnit(selectedUnitOption != null ? LengthUnit[selectedUnitOption.value] : null);
1345       });
1346   } else {
1347     disablePreferenceRow(dialog.unitSelect);
1348   }
1349 
1350   // CURRENCY
1351   dialog.currencySelect = dialog.getElement("currency-select");
1352   dialog.valueAddedTaxCheckBox = dialog.getElement("value-added-tax-checkbox");
1353   var noCurrencyLabel = dialog.getLocalizedLabelText("UserPreferencesPanel", "currencyComboBox.noCurrency.text");
1354   if (controller.isPropertyEditable("CURRENCY")) {
1355     dialog.currencySelect.appendChild(JSComponent.createOptionElement("", noCurrencyLabel, !controller.getCurrency()));
1356     var currencyDisplayNames = {
1357         EUR: "EUR €",
1358         AED: "AED AED",
1359         AFN: "AFN ؋",
1360         ALL: "ALL Lekë",
1361         AMD: "AMD ֏",
1362         ANG: "ANG NAf.",
1363         AOA: "AOA Kz",
1364         ARS: "ARS $",
1365         AUD: "AUD $",
1366         AWG: "AWG Afl.",
1367         AZN: "AZN ₼",
1368         BAM: "BAM KM",
1369         BBD: "BBD $",
1370         BDT: "BDT ৳",
1371         BGN: "BGN лв.",
1372         BHD: "BHD د.ب.‏",
1373         BIF: "BIF FBu",
1374         BMD: "BMD $",
1375         BND: "BND $",
1376         BOB: "BOB Bs",
1377         BRL: "BRL R$",
1378         BSD: "BSD $",
1379         BTN: "BTN Nu.",
1380         BWP: "BWP P",
1381         BYN: "BYN Br",
1382         BZD: "BZD $",
1383         CAD: "CAD $",
1384         CDF: "CDF FC",
1385         CHF: "CHF CHF",
1386         CLP: "CLP $",
1387         CNY: "CNY ¥",
1388         COP: "COP $",
1389         CRC: "CRC ₡",
1390         CSD: "CSD CSD",
1391         CUP: "CUP $",
1392         CVE: "CVE ​",
1393         CZK: "CZK Kč",
1394         DJF: "DJF Fdj",
1395         DKK: "DKK kr",
1396         DOP: "DOP RD$",
1397         DZD: "DZD DA",
1398         EGP: "EGP ج.م.‏",
1399         ERN: "ERN Nfk",
1400         ETB: "ETB Br",
1401         EUR: "EUR €",
1402         FJD: "FJD $",
1403         FKP: "FKP £",
1404         GBP: "GBP £",
1405         GEL: "GEL ₾",
1406         GHS: "GHS GH₵",
1407         GIP: "GIP £",
1408         GMD: "GMD D",
1409         GNF: "GNF FG",
1410         GTQ: "GTQ Q",
1411         GYD: "GYD $",
1412         HKD: "HKD HK$",
1413         HNL: "HNL L",
1414         HRK: "HRK HRK",
1415         HTG: "HTG G",
1416         HUF: "HUF Ft",
1417         IDR: "IDR Rp",
1418         ILS: "ILS ₪",
1419         INR: "INR ₹",
1420         IQD: "IQD د.ع.‏",
1421         IRR: "IRR IRR",
1422         ISK: "ISK ISK",
1423         JMD: "JMD $",
1424         JOD: "JOD د.أ.‏",
1425         JPY: "JPY ¥",
1426         KES: "KES Ksh",
1427         KGS: "KGS сом",
1428         KHR: "KHR ៛",
1429         KMF: "KMF CF",
1430         KPW: "KPW KPW",
1431         KRW: "KRW ₩",
1432         KWD: "KWD د.ك.‏",
1433         KYD: "KYD $",
1434         KZT: "KZT ₸",
1435         LAK: "LAK ₭",
1436         LBP: "LBP ل.ل.‏",
1437         LKR: "LKR Rs.",
1438         LRD: "LRD $",
1439         LSL: "LSL LSL",
1440         LYD: "LYD د.ل.‏",
1441         MAD: "MAD MAD",
1442         MDL: "MDL L",
1443         MGA: "MGA Ar",
1444         MKD: "MKD den",
1445         MMK: "MMK K",
1446         MNT: "MNT ₮",
1447         MOP: "MOP MOP$",
1448         MRU: "MRU UM",
1449         MUR: "MUR Rs",
1450         MWK: "MWK MK",
1451         MXN: "MXN $",
1452         MYR: "MYR RM",
1453         MZN: "MZN MTn",
1454         NAD: "NAD $",
1455         NGN: "NGN ₦",
1456         NIO: "NIO C$",
1457         NOK: "NOK kr",
1458         NPR: "NPR नेरू",
1459         NZD: "NZD $",
1460         OMR: "OMR ر.ع.‏",
1461         PAB: "PAB B/.",
1462         PEN: "PEN S/",
1463         PGK: "PGK K",
1464         PHP: "PHP ₱",
1465         PKR: "PKR ر",
1466         PLN: "PLN zł",
1467         PYG: "PYG Gs.",
1468         QAR: "QAR ر.ق.‏",
1469         RON: "RON RON",
1470         RSD: "RSD RSD",
1471         RUB: "RUB ₽",
1472         RWF: "RWF RF",
1473         SAR: "SAR ر.س.‏",
1474         SBD: "SBD $",
1475         SCR: "SCR SR",
1476         SDG: "SDG SDG",
1477         SEK: "SEK kr",
1478         SGD: "SGD $",
1479         SHP: "SHP £",
1480         SLL: "SLL Le",
1481         SOS: "SOS S",
1482         SRD: "SRD $",
1483         SSP: "SSP £",
1484         STN: "STN Db",
1485         SVC: "SVC C",
1486         SYP: "SYP LS",
1487         SZL: "SZL E",
1488         THB: "THB ฿",
1489         TJS: "TJS сом.",
1490         TMT: "TMT TMT",
1491         TND: "TND DT",
1492         TOP: "TOP T$",
1493         TRY: "TRY ₺",
1494         TTD: "TTD $",
1495         TWD: "TWD $",
1496         TZS: "TZS TSh",
1497         UAH: "UAH ₴",
1498         UGX: "UGX USh",
1499         USD: "USD $",
1500         UYU: "UYU $",
1501         UZS: "UZS сўм",
1502         VES: "VES Bs.S",
1503         VND: "VND ₫",
1504         VUV: "VUV VT",
1505         WST: "WST WS$",
1506         XAF: "XAF FCFA",
1507         XCD: "XCD $",
1508         XOF: "XOF CFA",
1509         XPF: "XPF FCFP",
1510         YER: "YER ر.ي.‏",
1511         ZAR: "ZAR R",
1512         ZMW: "ZMW K",
1513         ZWL: "ZWL ZWL"
1514       };
1515     var currencies = Object.keys(currencyDisplayNames);
1516     for (var i = 0; i < currencies.length; i++) {
1517       var currency = currencies[i];
1518       var currencyLabel = currencyDisplayNames[currency];
1519       dialog.currencySelect.appendChild(JSComponent.createOptionElement(
1520           currency, currencyLabel, currency == controller.getCurrency()));
1521     }
1522 
1523     dialog.registerEventListener(dialog.currencySelect, "change", function(ev) {
1524         var selectedIndex = dialog.currencySelect.selectedIndex;
1525         var selectedCurrency = dialog.currencySelect.options[selectedIndex].value;
1526         controller.setCurrency(selectedCurrency ? selectedCurrency : null);
1527       });
1528     dialog.registerPropertyChangeListener(controller, "CURRENCY", function(ev) {
1529         var option = dialog.currencySelect.querySelector("[value='" + (controller.getCurrency() ? controller.getCurrency() : "") + "']");
1530         option.selected = true;
1531         dialog.valueAddedTaxCheckBox.disabled = controller.getCurrency() == null;
1532       });
1533 
1534     // VALUE_ADDED_TAX_ENABLED
1535     var vatEnabled = controller.isPropertyEditable("VALUE_ADDED_TAX_ENABLED");
1536     dialog.valueAddedTaxCheckBox.parentElement.style.display = vatEnabled ? "initial" : "none";
1537     dialog.valueAddedTaxCheckBox.disabled = controller.getCurrency() == null;
1538     dialog.valueAddedTaxCheckBox.checked = controller.isValueAddedTaxEnabled();
1539     dialog.registerEventListener(dialog.valueAddedTaxCheckBox, "change", function(ev) {
1540         controller.setValueAddedTaxEnabled(dialog.valueAddedTaxCheckBox.checked);
1541       });
1542     dialog.registerPropertyChangeListener(controller, "VALUE_ADDED_TAX_ENABLED", function(ev) {
1543         dialog.valueAddedTaxCheckBox.disabled = controller.getCurrency() == null;
1544         dialog.valueAddedTaxCheckBox.checked = controller.isValueAddedTaxEnabled();
1545       });
1546   } else {
1547     disablePreferenceRow(dialog.currencySelect);
1548   }
1549 
1550   // FURNITURE_CATALOG_VIEWED_IN_TREE
1551   dialog.furnitureCatalogViewTreeRadioButton = dialog.findElement("[name='furniture-catalog-view-radio'][value='tree']");
1552   if (controller.isPropertyEditable("FURNITURE_CATALOG_VIEWED_IN_TREE") && false) {
1553     var selectedFurnitureCatalogView = controller.isFurnitureCatalogViewedInTree() ? "tree" : "list";
1554     dialog.findElement("[name='furniture-catalog-view-radio'][value='" + selectedFurnitureCatalogView + "']").checked = true;
1555   } else {
1556     disablePreferenceRow(dialog.furnitureCatalogViewTreeRadioButton);
1557   }
1558 
1559   // NAVIGATION_PANEL_VISIBLE 
1560   dialog.navigationPanelCheckBox = dialog.getElement("navigation-panel-checkbox");
1561   if (controller.isPropertyEditable("NAVIGATION_PANEL_VISIBLE")) {
1562     dialog.navigationPanelCheckBox.checked = controller.isNavigationPanelVisible();
1563   } else {
1564     disablePreferenceRow(dialog.navigationPanelCheckBox);
1565   }
1566   
1567   // EDITING_IN_3D_VIEW_ENABLED 
1568   dialog.editingIn3DViewCheckBox = dialog.getElement("editing-in-3D-view-checkbox");
1569   if (controller.isPropertyEditable("EDITING_IN_3D_VIEW_ENABLED")) {
1570     dialog.editingIn3DViewCheckBox.checked = controller.isEditingIn3DViewEnabled();
1571   } else {
1572     disablePreferenceRow(dialog.editingIn3DViewCheckBox);
1573   }
1574   
1575   // AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED
1576   dialog.aerialViewCenteredOnSelectionCheckBox = dialog.getElement("aerial-view-centered-on-selection-checkbox");
1577   if (controller.isPropertyEditable("AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED")) {
1578     dialog.aerialViewCenteredOnSelectionCheckBox.checked = controller.isAerialViewCenteredOnSelectionEnabled();
1579   } else {
1580     disablePreferenceRow(dialog.aerialViewCenteredOnSelectionCheckBox);
1581   }
1582   
1583   // OBSERVER_CAMERA_SELECTED_AT_CHANGE
1584   dialog.observerCameraSelectedAtChangeCheckBox = dialog.getElement("observer-camera-selected-at-change-checkbox");
1585   if (controller.isPropertyEditable("OBSERVER_CAMERA_SELECTED_AT_CHANGE")) {
1586     dialog.observerCameraSelectedAtChangeCheckBox.checked = controller.isObserverCameraSelectedAtChange();
1587   } else {
1588     disablePreferenceRow(dialog.observerCameraSelectedAtChangeCheckBox);
1589   }
1590   
1591   // MAGNETISM
1592   dialog.magnetismCheckBox = dialog.getElement("magnetism-checkbox");
1593   if (controller.isPropertyEditable("MAGNETISM_ENABLED")) {
1594     dialog.magnetismCheckBox.checked = controller.isMagnetismEnabled();
1595   } else {
1596     disablePreferenceRow(dialog.magnetismCheckBox);
1597   }
1598   
1599   // RULERS
1600   dialog.rulersCheckBox = dialog.getElement("rulers-checkbox");
1601   if (controller.isPropertyEditable("RULERS_VISIBLE") && false) {
1602     dialog.rulersCheckBox.checked = controller.isRulersVisible();
1603   } else {
1604     disablePreferenceRow(dialog.rulersCheckBox);
1605   }
1606   
1607   // GRID
1608   dialog.gridCheckBox = dialog.getElement("grid-checkbox");
1609   if (controller.isPropertyEditable("GRID_VISIBLE")) {
1610     dialog.gridCheckBox.checked = controller.isGridVisible();
1611   } else {
1612     disablePreferenceRow(dialog.gridCheckBox);
1613   }
1614 
1615   // DEFAULT_FONT_NAME
1616   dialog.defaultFontNameSelect = dialog.getElement("default-font-name-select");
1617   if (controller.isPropertyEditable("DEFAULT_FONT_NAME")) {
1618     var DEFAULT_SYSTEM_FONT_NAME = "DEFAULT_SYSTEM_FONT_NAME";
1619     var defaultFontChangeListener = function() {
1620       var selectedValue = controller.getDefaultFontName() == null ? DEFAULT_SYSTEM_FONT_NAME : controller.getDefaultFontName();
1621       var selectedOption = dialog.defaultFontNameSelect.querySelector("[value='" + selectedValue + "']")
1622       if (selectedOption) {
1623         selectedOption.selected = true;
1624       }
1625     };
1626 
1627     CoreTools.loadAvailableFontNames(function(fonts) {
1628       fonts = [DEFAULT_SYSTEM_FONT_NAME].concat(fonts);
1629       for (var i = 0; i < fonts.length; i++) {
1630         var font = fonts[i];
1631         var label = i == 0 ? dialog.getLocalizedLabelText("FontNameComboBox", "systemFontName") : font;
1632         dialog.defaultFontNameSelect.appendChild(JSComponent.createOptionElement(font, label));
1633       }
1634       defaultFontChangeListener();
1635     });
1636 
1637     dialog.registerPropertyChangeListener(controller, "DEFAULT_FONT_NAME", defaultFontChangeListener);
1638 
1639     dialog.registerEventListener(dialog.defaultFontNameSelect, "change", function(ev) {
1640         var selectedValue = dialog.defaultFontNameSelect.querySelector("option:checked").value;
1641         controller.setDefaultFontName(selectedValue == DEFAULT_SYSTEM_FONT_NAME ? null : selectedValue);
1642       });
1643   } else {
1644     disablePreferenceRow(dialog.defaultFontNameSelect);
1645   }
1646 
1647   // FURNITURE ICON 
1648   dialog.iconTopViewRadioButton = dialog.findElement("[name='furniture-icon-radio'][value='topView']");
1649   dialog.iconSizeSelect = dialog.getElement("icon-size-select");
1650   if (controller.isPropertyEditable("FURNITURE_VIEWED_FROM_TOP")) {
1651     var selectedIconMode = controller.isFurnitureViewedFromTop() ? "topView" : "catalog";
1652     dialog.findElement("[name='furniture-icon-radio'][value='" + selectedIconMode + "']").checked = true;
1653 
1654     var iconSizes = [128, 256, 512 ,1024];
1655     for (var i = 0; i < iconSizes.length; i++) {
1656       var size = iconSizes[i];
1657       dialog.iconSizeSelect.appendChild(
1658           JSComponent.createOptionElement(size, size + '×' + size,
1659               controller.getFurnitureModelIconSize() == size));
1660     }
1661 
1662     /**
1663      * Called when furniture icon mode is selected, in order to enable icon size if necessary
1664      * @private
1665      */
1666     var iconModeSelected = function(dialog) {
1667       dialog.iconSizeSelect.disabled = !dialog.iconTopViewRadioButton.checked;
1668     }
1669 
1670     iconModeSelected(dialog);
1671     dialog.registerEventListener(dialog.findElements("[name='furniture-icon-radio']"), "change", function(ev) {
1672         iconModeSelected(dialog);
1673       });
1674   } else {
1675     disablePreferenceRow(dialog.iconTopViewRadioButton);
1676   }
1677 
1678   // ROOM_FLOOR_COLORED_OR_TEXTURED
1679   dialog.roomRenderingFloorColorOrTextureRadioButton = dialog.findElement("[name='room-rendering-radio'][value='floorColorOrTexture']");
1680   if (controller.isPropertyEditable("ROOM_FLOOR_COLORED_OR_TEXTURED")) {
1681     var roomRenderingValue = controller.isRoomFloorColoredOrTextured() ? "floorColorOrTexture" : "monochrome";
1682     dialog.findElement("[name='room-rendering-radio'][value='" + roomRenderingValue + "']").checked = true;
1683   } else {
1684     disablePreferenceRow(dialog.roomRenderingFloorColorOrTextureRadioButton);
1685   }
1686 
1687   // NEW_WALL_PATTERN
1688   var newWallPatternSelect = dialog.getElement("new-wall-pattern-select");
1689   if (controller.isPropertyEditable("NEW_WALL_PATTERN")) {
1690     var patternsTexturesByURL = {};
1691     var patterns = preferences.getPatternsCatalog().getPatterns();
1692     for (var i = 0; i < patterns.length; i++) {
1693       var url = patterns[i].getImage().getURL();
1694       patternsTexturesByURL[url] = patterns[i];
1695     }
1696     newWallPatternSelect.classList.add("wall-pattern-combo-box");
1697     dialog.patternComboBox = new JSComboBox(preferences, newWallPatternSelect, 
1698         {
1699           availableValues: Object.keys(patternsTexturesByURL),
1700           renderCell: function(patternURL, patternItemElement) {
1701             patternItemElement.style.backgroundImage = "url('" + patternURL + "')";
1702           },
1703           selectionChanged: function(newValue) {
1704             controller.setNewWallPattern(patternsTexturesByURL[newValue]);
1705           }
1706         });
1707 
1708     var selectedUrl = (controller.getNewWallPattern() != null 
1709           ? controller.getNewWallPattern() 
1710           : controller.getWallPattern()).getImage().getURL();
1711     dialog.patternComboBox.setSelectedItem(selectedUrl);
1712     dialog.registerPropertyChangeListener(controller, "NEW_WALL_PATTERN", function() {
1713         var selectedUrl = controller.getNewWallPattern().getImage().getURL();
1714         dialog.patternComboBox.setSelectedItem(selectedUrl);
1715       });
1716   } else {
1717     disablePreferenceRow(dialog.newWallPatternSelect);
1718   }
1719 
1720   // NEW_WALL_THICKNESS
1721   dialog.newWallThicknessInput = new JSSpinner(preferences, dialog.getElement("new-wall-thickness-input"), 
1722       {
1723         value: 1,  
1724         minimum: 0, 
1725         maximum: 100000
1726       });
1727   if (controller.isPropertyEditable("NEW_WALL_THICKNESS")) {
1728     dialog.newWallThicknessInput.setValue(controller.getNewWallThickness());
1729   } else {
1730     disablePreferenceRow(dialog.newWallThicknessInput);
1731   }
1732 
1733   // NEW_WALL_HEIGHT
1734   dialog.newWallHeightInput = new JSSpinner(preferences, dialog.getElement("new-wall-height-input"), 
1735       {
1736         value: 1,  
1737         minimum: 0, 
1738         maximum: 100000
1739       });
1740   if (controller.isPropertyEditable("NEW_WALL_HEIGHT")) {
1741     dialog.newWallHeightInput.setValue(controller.getNewWallHeight());
1742   } else {
1743     disablePreferenceRow(dialog.newWallHeightInput);
1744   }
1745 
1746   // NEW_FLOOR_THICKNESS
1747   dialog.newFloorThicknessInput = new JSSpinner(preferences, dialog.getElement("new-floor-thickness-input"), 
1748       {
1749         value: 1,  
1750         minimum: 0, 
1751         maximum: 100000
1752       });
1753   if (controller.isPropertyEditable("NEW_FLOOR_THICKNESS")) {
1754     dialog.newFloorThicknessInput.setValue(controller.getNewFloorThickness());
1755   } else {
1756     disablePreferenceRow(dialog.newFloorThicknessInput);
1757   }
1758 
1759   var updateSpinnerStepsAndLength = function(spinner, centimeterStepSize, inchStepSize) {
1760       if (controller.getUnit().isMetric()) {
1761         spinner.setStepSize(centimeterStepSize);
1762       } else {
1763         spinner.setStepSize(inchStepSize);
1764       }
1765       spinner.setMinimum(controller.getUnit().getMinimumLength());
1766       if (spinner.getMinimum() > spinner.getValue()) {
1767         spinner.setValue(spinner.getMinimum());
1768       }
1769       spinner.setFormat(controller.getUnit().getFormat());
1770     };
1771 
1772   var updateStepsAndLength = function() {
1773       updateSpinnerStepsAndLength(dialog.newWallThicknessInput, LengthUnit.CENTIMETER.getStepSize(), LengthUnit.INCH.getStepSize());
1774       updateSpinnerStepsAndLength(dialog.newWallHeightInput, LengthUnit.CENTIMETER.getStepSize() * 20, LengthUnit.INCH.getStepSize() * 16);
1775       updateSpinnerStepsAndLength(dialog.newFloorThicknessInput, LengthUnit.CENTIMETER.getStepSize(), LengthUnit.INCH.getStepSize());
1776     };
1777 
1778   updateStepsAndLength();
1779   dialog.registerPropertyChangeListener(controller, "UNIT", function(ev) {
1780       updateStepsAndLength();
1781     });
1782   return dialog;
1783 }
1784 
1785 JSViewFactory.prototype.createLevelView = function(preferences, controller) {
1786   var dialog = new JSDialog(preferences,
1787       "@{LevelPanel.level.title}",
1788       document.getElementById("level-dialog-template"), 
1789       {
1790         size: "medium",
1791         applier: function() {
1792           controller.modifyLevels();
1793         }
1794       });
1795 
1796   var unitName = preferences.getLengthUnit().getName();
1797 
1798   // Viewable check box bound to VIEWABLE controller property
1799   var viewableCheckBox = dialog.getElement("viewable-checkbox");
1800   var viewableCheckBoxDisplay = controller.isPropertyEditable("VIEWABLE") ? "initial" : "none";
1801   viewableCheckBox.parentElement.style.display = viewableCheckBoxDisplay;
1802   viewableCheckBox.checked = controller.getViewable();
1803   dialog.registerEventListener(viewableCheckBox, "change", function(ev) {
1804       controller.setViewable(viewableCheckBox.checked);
1805     });
1806   dialog.registerPropertyChangeListener(controller, "VIEWABLE", function(ev) {
1807       viewableCheckBox.checked = controller.getViewable();
1808     });
1809 
1810   // Name text field bound to NAME controller property
1811   var nameInput = dialog.getElement("name-input");
1812   var nameDisplay = controller.isPropertyEditable("NAME") ? "initial" : "none";
1813   nameInput.parentElement.style.display = nameDisplay;
1814   nameInput.parentElement.previousElementSibling.style.display = nameDisplay;
1815   nameInput.value = controller.getName() != null ? controller.getName() : "";
1816   dialog.registerEventListener(nameInput, "input", function(ev) {
1817       var name = nameInput.value;
1818       if (name.trim().length == 0) {
1819         controller.setName(null);
1820       } else {
1821         controller.setName(name);
1822       }
1823     });
1824   dialog.registerPropertyChangeListener(controller, "NAME", function(ev) {
1825       nameInput.value = controller.getName() != null ? controller.getName() : "";
1826     });
1827 
1828   // Elevation spinner bound to ELEVATION controller property
1829   var minimumLength = preferences.getLengthUnit().getMinimumLength();
1830   var maximumLength = preferences.getLengthUnit().getMaximumLength();
1831   var setFloorThicknessEnabled = function() {
1832       var selectedLevelIndex = controller.getSelectedLevelIndex();
1833       if (selectedLevelIndex != null) {
1834         var levels = controller.getLevels();
1835         dialog.floorThicknessInput.setEnabled(levels[selectedLevelIndex].getElevation() != levels[0].getElevation());
1836       }
1837     };
1838   var setElevationIndexButtonsEnabled = function() {
1839       var selectedLevelIndex = controller.getSelectedLevelIndex();
1840       if (selectedLevelIndex != null) {
1841         var levels = controller.getLevels();
1842         dialog.increaseElevationButton.disabled = !(selectedLevelIndex < levels.length - 1
1843             && levels [selectedLevelIndex].getElevation() == levels [selectedLevelIndex + 1].getElevation());
1844         dialog.decreaseElevationButton.disabled = !(selectedLevelIndex > 0
1845             && levels [selectedLevelIndex].getElevation() == levels [selectedLevelIndex - 1].getElevation());
1846       } else {
1847         dialog.increaseElevationButton.setEnabled(false);
1848         dialog.decreaseElevationButton.setEnabled(false);
1849       }
1850     };
1851 
1852   var elevationDisplay = controller.isPropertyEditable("ELEVATION") ? "initial" : "none";
1853   dialog.getElement("elevation-label").textContent = dialog.getLocalizedLabelText("LevelPanel", "elevationLabel.text", unitName);
1854   var elevationInput = new JSSpinner(preferences, dialog.getElement("elevation-input"), 
1855       {
1856         nullable: controller.getElevation() == null,
1857         format: preferences.getLengthUnit().getFormat(),
1858         value: controller.getElevation(),
1859         minimum: -1000,
1860         maximum: preferences.getLengthUnit().getMaximumElevation(),
1861         stepSize: preferences.getLengthUnit().getStepSize()
1862       });
1863   elevationInput.parentElement.style.display = elevationDisplay;
1864   elevationInput.parentElement.previousElementSibling.style.display = elevationDisplay;
1865   dialog.registerEventListener(elevationInput, "input", function(ev) {
1866       controller.setElevation(elevationInput.getValue());
1867       setFloorThicknessEnabled();
1868       setElevationIndexButtonsEnabled();
1869     });
1870   dialog.registerPropertyChangeListener(controller, "ELEVATION", function(ev) {
1871       elevationInput.setValue(ev.getNewValue());
1872     });
1873 
1874   var floorThicknessDisplay = controller.isPropertyEditable("FLOOR_THICKNESS") ? "initial" : "none";
1875   dialog.getElement("floor-thickness-label").textContent = dialog.getLocalizedLabelText("LevelPanel", "floorThicknessLabel.text", unitName);
1876   var floorThicknessInput = new JSSpinner(preferences, dialog.getElement("floor-thickness-input"), 
1877       {
1878         nullable: controller.getFloorThickness() == null,
1879         format: preferences.getLengthUnit().getFormat(),
1880         value: controller.getFloorThickness(),
1881         minimum: minimumLength,
1882         maximum: maximumLength / 10,
1883         stepSize: preferences.getLengthUnit().getStepSize(),
1884       });
1885   floorThicknessInput.parentElement.style.display = floorThicknessDisplay;
1886   floorThicknessInput.parentElement.previousElementSibling.style.display = floorThicknessDisplay;
1887   dialog.registerEventListener(floorThicknessInput, "input", function(ev) {
1888       controller.setFloorThickness(floorThicknessInput.getValue());
1889     });
1890   dialog.registerPropertyChangeListener(controller, "FLOOR_THICKNESS", function(ev) {
1891       floorThicknessInput.setValue(ev.getNewValue());
1892     });
1893   dialog.floorThicknessInput = floorThicknessInput;
1894   setFloorThicknessEnabled(controller);
1895 
1896   var heightDisplay = controller.isPropertyEditable("HEIGHT") ? "initial" : "none";
1897   dialog.getElement("height-label").textContent = dialog.getLocalizedLabelText("LevelPanel", "heightLabel.text", unitName);
1898   var heightInput = new JSSpinner(preferences, dialog.getElement("height-input"), 
1899       {
1900         nullable: controller.getHeight() == null,
1901         format: preferences.getLengthUnit().getFormat(),
1902         value: controller.getHeight(),
1903         minimum: minimumLength,
1904         maximum: maximumLength,
1905         stepSize: preferences.getLengthUnit().getStepSize()
1906       });
1907   heightInput.parentElement.style.display = heightDisplay;
1908   heightInput.parentElement.previousElementSibling.style.display = heightDisplay;
1909   dialog.registerEventListener(heightInput, "input", function(ev) {
1910       controller.setHeight(heightInput.getValue());
1911     });
1912   dialog.registerPropertyChangeListener(controller, "HEIGHT", function(ev) {
1913       heightInput.setValue(ev.getNewValue());
1914     });
1915 
1916   var elevationButtonsDisplay = controller.isPropertyEditable("ELEVATION_INDEX") ? "initial" : "none";
1917   var increaseElevationButtonAction = new ResourceAction(preferences, "LevelPanel", "INCREASE_ELEVATION_INDEX", true);
1918   var decreaseElevationButtonAction = new ResourceAction(preferences, "LevelPanel", "DECREASE_ELEVATION_INDEX", true);
1919   dialog.increaseElevationButton = dialog.getElement("increase-elevation-index-button");
1920   dialog.increaseElevationButton.style.backgroundImage = "url('" + increaseElevationButtonAction.getURL(AbstractAction.SMALL_ICON) + "')";
1921   dialog.increaseElevationButton.style.display = elevationButtonsDisplay;
1922   dialog.registerEventListener(dialog.increaseElevationButton, "click", function(ev) {
1923       controller.setElevationIndex(controller.getElevationIndex() + 1);
1924       setElevationIndexButtonsEnabled();
1925     });
1926 
1927   dialog.decreaseElevationButton = dialog.getElement("decrease-elevation-index-button");
1928   dialog.decreaseElevationButton.style.backgroundImage = "url('" + decreaseElevationButtonAction.getURL(AbstractAction.SMALL_ICON) + "')";
1929   dialog.decreaseElevationButton.style.display = elevationButtonsDisplay;
1930   dialog.registerEventListener(dialog.decreaseElevationButton, "click", function(ev) {
1931       controller.setElevationIndex(controller.getElevationIndex() - 1);
1932       setElevationIndexButtonsEnabled();
1933     });
1934 
1935   setElevationIndexButtonsEnabled();
1936 
1937   var levelsTableBody = dialog.getElement("levels-table").querySelector("tbody");
1938 
1939   var updateSelectedLevel = function() {
1940     var selectedLevelIndex = controller.getSelectedLevelIndex();
1941 
1942     var selectedLevelRow = levelsTableBody.querySelector(".selected");
1943     if (selectedLevelRow != null) {
1944       selectedLevelRow.classList.remove("selected");
1945     }
1946 
1947     if (selectedLevelIndex != null) {
1948       // Levels are listed in the table in reverse order
1949       var rowIndex = levelsTableBody.childElementCount - selectedLevelIndex - 1;
1950       levelsTableBody.children[rowIndex].classList.add("selected");
1951     }
1952   };
1953 
1954   var generateTableBody = function() {
1955       var levels = controller.getLevels();
1956       var bodyHtml = "";
1957   
1958       var lengthFormat = preferences.getLengthUnit().getFormat();
1959       for (var i = levels.length - 1; i >= 0; i--) {
1960         var level = levels[i];
1961         var disabledAttribute = level.isViewable() ? "" : "disabled";
1962         bodyHtml += 
1963               "<tr " + disabledAttribute + ">"
1964             + "  <td>" + level.getName() + "</td>"
1965             + "  <td>" + lengthFormat.format(level.getElevation()) + "</td>"
1966             + "  <td>" + (level.getElevation() == levels [0].getElevation() ? "" : lengthFormat.format(level.getFloorThickness())) + "</td>"
1967             + "  <td>" + lengthFormat.format(level.getHeight()) + "</td>"
1968             + "</tr>";
1969       }
1970   
1971       levelsTableBody.innerHTML = bodyHtml;
1972       updateSelectedLevel();
1973     };
1974 
1975   generateTableBody();
1976 
1977   dialog.registerPropertyChangeListener(controller, "SELECT_LEVEL_INDEX", updateSelectedLevel);
1978   dialog.registerPropertyChangeListener(controller, "LEVELS", generateTableBody);
1979   return dialog;
1980 }
1981 
1982 /**
1983  * 
1984  * @param {UserPreferences} preferences 
1985  * @param {HomeFurnitureController} controller
1986  */
1987 JSViewFactory.prototype.createHomeFurnitureView = function(preferences, controller) {
1988   function HomeFurnitureDialog() {
1989     this.controller = controller;
1990     
1991     JSDialog.call(this, preferences, 
1992       "@{HomeFurniturePanel.homeFurniture.title}", 
1993       document.getElementById("home-furniture-dialog-template"),
1994       {
1995         applier: function() {
1996           controller.modifyFurniture();
1997         },
1998         disposer: function(dialog) {
1999           dialog.paintPanel.colorButton.dispose();
2000           dialog.paintPanel.textureComponent.dispose();
2001         }
2002       });
2003 
2004     this.initNameAndPricePanel();
2005     this.initLocationPanel();
2006     this.initPaintPanel();
2007     this.initOrientationPanel();
2008     this.initSizePanel();
2009     this.initShininessPanel();
2010 
2011     var dialog = this;
2012     if (this.controller.isPropertyEditable("VISIBLE")) {
2013       // Create visible check box bound to VISIBLE controller property
2014       var visibleCheckBox = this.getElement("visible-checkbox");
2015       visibleCheckBox.checked = this.controller.getVisible();
2016       this.registerPropertyChangeListener(this.controller, "VISIBLE", function(ev) {
2017           visibleCheckBox.checked = ev.getNewValue();
2018         });
2019 
2020       this.registerEventListener(visibleCheckBox, "change", function(ev) {
2021             dialog.controller.setVisible(visibleCheckBox.checked);
2022           });
2023     }
2024 
2025     // must be done at last, needs multiple components to be initialized
2026     if (this.controller.isPropertyEditable("PAINT")) {
2027       this.updatePaintRadioButtons();
2028     }
2029   }
2030   HomeFurnitureDialog.prototype = Object.create(JSDialog.prototype);
2031   HomeFurnitureDialog.prototype.constructor = HomeFurnitureDialog;
2032 
2033   /**
2034    * @private
2035    */
2036   HomeFurnitureDialog.prototype.initNameAndPricePanel = function() {
2037     var title = this.getElement("name-and-price-title");
2038     title.textContent = this.getLocalizedLabelText(
2039         "HomeFurniturePanel", controller.isPropertyEditable("PRICE") ? "nameAndPricePanel.title" : "namePanel.title");
2040 
2041     var nameLabel = this.getElement("name-label");
2042     var nameInput = this.getElement("name-input");
2043     var nameVisibleCheckBox = this.getElement("name-visible-checkbox");
2044     var descriptionLabel = this.getElement("description-label");
2045     var descriptionInput = this.getElement("description-input");
2046     var priceLabel = this.getElement("price-label");
2047     var priceInput = new JSSpinner(this.preferences, this.getElement("price-input"), 
2048         {
2049           nullable: this.controller.getPrice() == null,
2050           value: 0, 
2051           minimum: 0, 
2052           maximum: 1000000000
2053         });
2054     var valueAddedTaxPercentageInput = new JSSpinner(this.preferences, this.getElement("value-added-tax-percentage-input"), 
2055         { 
2056           nullable: this.controller.getValueAddedTaxPercentage() == null,
2057           value: 0, 
2058           minimum: 0, 
2059           maximum: 100, 
2060           stepSize: 0.5
2061         });
2062 
2063     // 1) Adjust visibility
2064     var nameDisplay = this.controller.isPropertyEditable("NAME") ? "initial" : "none";
2065     var nameVisibleDisplay = this.controller.isPropertyEditable("NAME_VISIBLE") ? "initial" : "none";
2066     var descriptionDisplay = this.controller.isPropertyEditable("DESCRIPTION") ? "initial" : "none";
2067     var priceDisplay = this.controller.isPropertyEditable("PRICE") ? "inline-block" : "none";
2068     var vatDisplay = this.controller.isPropertyEditable("VALUE_ADDED_TAX_PERCENTAGE") ? "inline-block" : "none";
2069 
2070     nameLabel.style.display = nameDisplay;
2071     nameInput.style.display = nameDisplay;
2072     
2073     nameVisibleCheckBox.parentElement.style.display = nameVisibleDisplay;
2074     
2075     descriptionLabel.style.display = descriptionDisplay;
2076     descriptionInput.style.display = descriptionDisplay;
2077     descriptionLabel.parentElement.style.display = descriptionDisplay; 
2078     descriptionInput.parentElement.style.display = descriptionDisplay;
2079     
2080     priceLabel.style.display = priceDisplay;
2081     priceInput.style.display = priceDisplay;
2082     priceLabel.parentElement.style.display = priceDisplay; 
2083     priceInput.parentElement.style.display = priceDisplay;
2084 
2085     valueAddedTaxPercentageInput.getHTMLElement().previousElementSibling.style.display = vatDisplay;
2086     valueAddedTaxPercentageInput.style.display = vatDisplay;
2087 
2088     // 2) Set values
2089     nameInput.value = controller.getName() != null ? controller.getName() : "";
2090     nameVisibleCheckBox.checked = this.controller.getNameVisible();
2091     descriptionInput.value = controller.getDescription() != null ? controller.getDescription() : "";
2092     priceInput.setValue(this.controller.getPrice());
2093     if (this.controller.getValueAddedTaxPercentage()) {
2094       valueAddedTaxPercentageInput.setValue(this.controller.getValueAddedTaxPercentage() * 100);
2095     }
2096 
2097     // 3) Add property listeners
2098     this.registerPropertyChangeListener(this.controller, "NAME", function(ev) {
2099         nameInput.value = controller.getName() != null ? controller.getName() : "";
2100       });
2101     this.registerPropertyChangeListener(this.controller, "NAME_VISIBLE", function(ev) {
2102         nameVisibleCheckBox.checked = controller.getNameVisible();
2103       });
2104     this.registerPropertyChangeListener(this.controller, "DESCRIPTION", function(ev) {
2105         descriptionInput.value = controller.getDescription() != null ? controller.getDescription() : "";
2106       });
2107     this.registerPropertyChangeListener(this.controller, "PRICE", function(ev) {
2108         priceInput.setValue(controller.getPrice());
2109       });
2110     this.registerPropertyChangeListener(this.controller, "VALUE_ADDED_TAX_PERCENTAGE", function(ev) {
2111         if (controller.getValueAddedTaxPercentage()) {
2112           valueAddedTaxPercentageInput.setValue(controller.getValueAddedTaxPercentage() * 100);
2113         } else {
2114           valueAddedTaxPercentageInput.setValue(null);
2115         }
2116       });
2117 
2118     // 4) Add change listeners
2119     this.registerEventListener(nameInput, "input", function(ev) {
2120         var name = nameInput.value;
2121         if (name.trim().length == 0) {
2122           controller.setName(null);
2123         } else {
2124           controller.setName(name);
2125         }
2126       });
2127     this.registerEventListener(nameVisibleCheckBox, "change", function(ev) {
2128         controller.setNameVisible(nameVisibleCheckBox.checked);
2129       });
2130     this.registerEventListener(descriptionInput, "input", function(ev) {
2131         var description = descriptionInput.value;
2132         if (description.trim().length == 0) {
2133           controller.setDescription(null);
2134         } else {
2135           controller.setDescription(description);
2136         }
2137       });
2138     this.registerEventListener(priceInput, "input", function(ev) {
2139         controller.setPrice(priceInput.getValue() != null  
2140             ? new Big(priceInput.getValue()) 
2141             : null);
2142       });
2143     this.registerEventListener(valueAddedTaxPercentageInput, "input", function(ev) {
2144         var vat = valueAddedTaxPercentageInput.getValue();
2145         controller.setValueAddedTaxPercentage(vat != null 
2146             ? new Big(vat / 100) 
2147             : null);
2148       });
2149   }
2150 
2151   /**
2152    * @private
2153    */
2154   HomeFurnitureDialog.prototype.initLocationPanel = function() {
2155     var xLabel = this.getElement("x-label");
2156     var xInput = new JSSpinner(this.preferences, this.getElement("x-input"), 
2157         {
2158           nullable: this.controller.getX() == null,
2159           format: this.preferences.getLengthUnit().getFormat(),
2160           stepSize: this.preferences.getLengthUnit().getStepSize()
2161         });
2162     var yLabel = this.getElement("y-label");
2163     var yInput = new JSSpinner(this.preferences, this.getElement("y-input"), 
2164         {
2165           nullable: this.controller.getY() == null,
2166           format: this.preferences.getLengthUnit().getFormat(),
2167           stepSize: this.preferences.getLengthUnit().getStepSize()
2168         });
2169     var elevationLabel = this.getElement("elevation-label");
2170     var elevationInput = new JSSpinner(this.preferences, this.getElement("elevation-input"), 
2171         {
2172           nullable: this.controller.getElevation() == null,
2173           format: this.preferences.getLengthUnit().getFormat(),
2174           stepSize: this.preferences.getLengthUnit().getStepSize()
2175         });
2176 
2177     var mirroredModelCheckBox = this.getElement("mirrored-model-checkbox");
2178     var basePlanItemCheckBox = this.getElement("base-plan-item-checkbox");
2179 
2180     // 1) Adjust visibility
2181     var xDisplay = this.controller.isPropertyEditable("X") ? "initial" : "none";
2182     var yDisplay = this.controller.isPropertyEditable("Y") ? "initial" : "none";
2183     var elevationDisplay = this.controller.isPropertyEditable("ELEVATION") ? "initial" : "none";
2184     var modelMirroredDisplay = this.controller.isPropertyEditable("MODEL_MIRRORED") ? "initial" : "none";
2185     var basePlanItemDisplay = this.controller.isPropertyEditable("BASE_PLAN_ITEM") ? "initial" : "none";
2186 
2187     xLabel.style.display = xDisplay;
2188     xInput.getHTMLElement().parentElement.style.display = xDisplay;
2189     yLabel.style.display = yDisplay;
2190     yInput.getHTMLElement().parentElement.style.display = yDisplay;
2191     elevationLabel.style.display =  elevationDisplay;
2192     elevationInput.getHTMLElement().parentElement.style.display = elevationDisplay;
2193     
2194     mirroredModelCheckBox.parentElement.style.display = modelMirroredDisplay;
2195     basePlanItemCheckBox.parentElement.style.display = basePlanItemDisplay;
2196 
2197     // 2) Set values
2198     xInput.setValue(this.controller.getX());
2199     yInput.setValue(this.controller.getY());
2200     elevationInput.setValue(this.controller.getElevation());
2201     mirroredModelCheckBox.checked = this.controller.getModelMirrored();
2202     basePlanItemCheckBox.checked = this.controller.getBasePlanItem();
2203 
2204     // 3) Set labels
2205     var unitName = this.preferences.getLengthUnit().getName();
2206     xLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "xLabel.text", unitName);
2207     yLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "yLabel.text", unitName);
2208     elevationLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "elevationLabel.text", unitName);
2209     
2210     // 4) Set custom attributes
2211     var maximumLength = this.preferences.getLengthUnit().getMaximumLength();
2212     var maximumElevation = this.preferences.getLengthUnit().getMaximumElevation();
2213     xInput.setMinimum(-maximumLength);
2214     yInput.setMinimum(-maximumLength);
2215     xInput.setMaximum(maximumLength);
2216     yInput.setMaximum(maximumLength); 
2217     elevationInput.setMinimum(0);
2218     elevationInput.setMaximum(maximumElevation);
2219 
2220     // 5) add property listeners
2221     var controller = this.controller;
2222     this.registerPropertyChangeListener(this.controller, "X", function(ev) {
2223         xInput.setValue(controller.getX());
2224       });
2225     this.registerPropertyChangeListener(this.controller, "Y", function(ev) {
2226         yInput.setValue(controller.getY());
2227       });
2228     this.registerPropertyChangeListener(this.controller, "ELEVATION", function(ev) {
2229         elevationInput.setValue(controller.getElevation());
2230       });
2231     this.registerPropertyChangeListener(this.controller, "MODEL_MIRRORED", function(ev) {
2232         mirroredModelCheckBox.checked = controller.getModelMirrored();
2233       });
2234     this.registerPropertyChangeListener(this.controller, "BASE_PLAN_ITEM", function(ev) {
2235         basePlanItemCheckBox.checked = controller.getBasePlanItem();
2236       });
2237 
2238     // 6) Add change listeners
2239     this.registerEventListener(xInput, "input", function(ev) {
2240           controller.setX(xInput.getValue());
2241         });
2242     this.registerEventListener(yInput, "input", function(ev) {
2243         controller.setY(yInput.getValue());
2244       });
2245     this.registerEventListener(elevationInput, "input", function(ev) {
2246         controller.setElevation(elevationInput.getValue());
2247       });
2248     this.registerEventListener(mirroredModelCheckBox, "change", function(ev) {
2249         controller.setModelMirrored(mirroredModelCheckBox.checked);
2250       });
2251     this.registerEventListener(basePlanItemCheckBox, "change", function(ev) {
2252         controller.setBasePlanItem(basePlanItemCheckBox.checked);
2253       });
2254 
2255     this.locationPanel = {
2256       element: this.findElement('.location-panel'),
2257       elevationInput: elevationInput
2258     };
2259   }
2260 
2261   /**
2262    * @private
2263    */
2264   HomeFurnitureDialog.prototype.initOrientationPanel = function() {
2265     var controller = this.controller;
2266 
2267     var angleLabel = this.getElement("angle-label");
2268     var angleDecimalFormat = new DecimalFormat("0.#");
2269     var angleInput = new JSSpinner(this.preferences, this.getElement("angle-input"), 
2270         {
2271           nullable: this.controller.getAngle() == null,
2272           format: angleDecimalFormat,
2273           minimum: 0,
2274           maximum: 360
2275         });
2276     var rollRadioButton = this.findElement("[name='horizontal-rotation-radio'][value='ROLL']");
2277     var pitchRadioButton = this.findElement("[name='horizontal-rotation-radio'][value='PITCH']");
2278     var rollInput = new JSSpinner(this.preferences, this.getElement("roll-input"), 
2279         {
2280           nullable: this.controller.getRoll() == null,
2281           format: angleDecimalFormat,
2282           minimum: 0,
2283           maximum: 360
2284         });
2285     var pitchInput = new JSSpinner(this.preferences, this.getElement("pitch-input"), 
2286         {
2287           nullable: this.controller.getPitch() == null,
2288           format: angleDecimalFormat,
2289           minimum: 0,
2290           maximum: 360
2291         });
2292 
2293     var verticalRotationLabel = this.getElement("vertical-rotation-label");
2294     var horizontalRotationLabel = this.getElement("horizontal-rotation-label");
2295     var furnitureOrientationImage = this.getElement("furniture-orientation-image");
2296     furnitureOrientationImage.innerHTML = "<img src='" + ZIPTools.getScriptFolder() + "resources/furnitureOrientation.png' />";
2297 
2298     // 1) Adjust visibility
2299     var angleDisplay = this.controller.isPropertyEditable("ANGLE_IN_DEGREES") || this.controller.isPropertyEditable("ANGLE") ? "initial" : "none";
2300     var rollDisplay = this.controller.isPropertyEditable("ROLL") ? "initial" : "none";
2301     var pitchDisplay = this.controller.isPropertyEditable("PITCH") ? "initial" : "none";
2302 
2303     var rollAndPitchDisplayed = this.controller.isPropertyEditable("ROLL") && this.controller.isPropertyEditable("PITCH");
2304     var verticalRotationLabelDisplay = rollAndPitchDisplayed ? "initial" : "none";
2305     var horizontalRotationLabelDisplay = verticalRotationLabelDisplay;
2306     var furnitureOrientationImageDisplay = this.controller.isTexturable() && rollAndPitchDisplayed ? "initial" : "none";
2307 
2308     angleLabel.style.display = angleDisplay;
2309     angleInput.getHTMLElement().parentElement.style.display = angleDisplay;
2310 
2311     rollRadioButton.parentElement.style.display = rollDisplay; 
2312     rollInput.getHTMLElement().parentElement.style.display = rollDisplay; 
2313     
2314     pitchRadioButton.parentElement.style.display = pitchDisplay; 
2315     pitchInput.getHTMLElement().parentElement.style.display = pitchDisplay; 
2316 
2317     horizontalRotationLabel.style.display = horizontalRotationLabelDisplay;    
2318     verticalRotationLabel.style.display = verticalRotationLabelDisplay;
2319     furnitureOrientationImage.style.display = furnitureOrientationImageDisplay;
2320 
2321     // 2) Set values
2322     if (this.controller.getAngle() != null) {
2323       angleInput.setValue(Math.toDegrees(this.controller.getAngle()));
2324     } else {
2325       angleInput.setValue(null);
2326     }
2327     if (this.controller.getRoll() != null) {
2328       rollInput.setValue(Math.toDegrees(this.controller.getRoll()));
2329     } else {
2330       rollInput.setValue(null);
2331     }
2332     if (this.controller.getPitch() != null) {
2333       pitchInput.setValue(Math.toDegrees(this.controller.getPitch()));
2334     } else {
2335       pitchInput.setValue(null);
2336     }
2337 
2338     var updateHorizontalAxisRadioButtons = function() {
2339         rollRadioButton.checked = controller.getHorizontalAxis() == HomeFurnitureController.FurnitureHorizontalAxis.ROLL;
2340         pitchRadioButton.checked = controller.getHorizontalAxis() == HomeFurnitureController.FurnitureHorizontalAxis.PITCH;
2341       };
2342     updateHorizontalAxisRadioButtons();
2343 
2344     // 3) Add property listeners
2345     this.registerPropertyChangeListener(this.controller, "ANGLE", function(ev) {
2346         if (controller.getAngle() != null) {
2347           angleInput.setValue(Math.toDegrees(controller.getAngle()));
2348         } else {
2349           angleInput.setValue(null);
2350         }
2351       });
2352     this.registerPropertyChangeListener(this.controller, "ROLL", function(ev) {
2353         if (controller.getRoll() != null) {
2354           rollInput.setValue(Math.toDegrees(controller.getRoll()));
2355         } else {
2356           rollInput.setValue(null);
2357         }
2358       });
2359     this.registerPropertyChangeListener(this.controller, "PITCH", function(ev) {
2360         if (controller.getPitch() != null) {
2361           pitchInput.setValue(Math.toDegrees(controller.getPitch()));
2362         } else {
2363           pitchInput.setValue(null);
2364         }
2365       });
2366     this.registerPropertyChangeListener(this.controller, "HORIZONTAL_AXIS", function(ev) {
2367         updateHorizontalAxisRadioButtons();
2368       });
2369 
2370     // 4) Add change listeners
2371     this.registerEventListener(angleInput, "input", function(ev) {
2372         if (angleInput.getValue() == null) {
2373           controller.setAngle(null);
2374         } else {
2375           controller.setAngle(Math.toRadians(angleInput.getValue()));
2376         }
2377       });
2378     this.registerEventListener(rollInput, "input", function(ev) {
2379         if (rollInput.getValue() == null) {
2380           controller.setRoll(null);
2381         } else {
2382           controller.setRoll(Math.toRadians(rollInput.getValue()));
2383           controller.setHorizontalAxis(HomeFurnitureController.FurnitureHorizontalAxis.ROLL);
2384         }
2385       });
2386     this.registerEventListener(pitchInput, "input", function(ev) {
2387         if (pitchInput.getValue() == null) {
2388           controller.setPitch(null);
2389         } else {
2390           controller.setPitch(Math.toRadians(pitchInput.getValue()));
2391           controller.setHorizontalAxis(HomeFurnitureController.FurnitureHorizontalAxis.PITCH);
2392         }
2393       });
2394     this.registerEventListener([rollRadioButton, pitchRadioButton], "change", function(ev) {
2395         if (rollRadioButton.checked) {
2396           controller.setHorizontalAxis(HomeFurnitureController.FurnitureHorizontalAxis.ROLL);
2397         } else {
2398           controller.setHorizontalAxis(HomeFurnitureController.FurnitureHorizontalAxis.PITCH);
2399         }
2400       });
2401 
2402     if (!rollAndPitchDisplayed) {
2403       this.findElement('.orientation-column').style.display = 'none';
2404       this.locationPanel.elevationInput.parentElement.insertAdjacentElement('afterend', angleLabel);
2405       angleLabel.insertAdjacentElement('afterend', angleInput.parentElement);
2406     }
2407   }
2408 
2409   /**
2410    * @private
2411    */
2412   HomeFurnitureDialog.prototype.initPaintPanel = function() {
2413     var dialog = this;
2414     var controller = this.controller;
2415     var preferences = this.preferences;
2416 
2417     var colorButton = new ColorButton(preferences, 
2418         {
2419           colorChanged: function(color) {
2420               colorAndTextureRadioButtons[HomeFurnitureController.FurniturePaint.COLORED].checked = true;
2421               controller.setPaint(HomeFurnitureController.FurniturePaint.COLORED);
2422               controller.setColor(color);
2423             }
2424         });
2425     this.attachChildComponent("color-button", colorButton);
2426     colorButton.setColor(controller.getColor());
2427     colorButton.setColorDialogTitle(preferences.getLocalizedString("HomeFurniturePanel", "colorDialog.title"));
2428 
2429     var textureComponent = controller.getTextureController().getView();
2430     this.attachChildComponent("texture-component", textureComponent);
2431 
2432     var selectedPaint = controller.getPaint();
2433 
2434     var colorAndTextureRadioButtons = [];
2435     colorAndTextureRadioButtons[HomeFurnitureController.FurniturePaint.DEFAULT] = dialog.findElement("[name='paint-checkbox'][value='default']");
2436     colorAndTextureRadioButtons[HomeFurnitureController.FurniturePaint.COLORED] = dialog.findElement("[name='paint-checkbox'][value='color']");
2437     colorAndTextureRadioButtons[HomeFurnitureController.FurniturePaint.TEXTURED] = dialog.findElement("[name='paint-checkbox'][value='texture']");
2438     colorAndTextureRadioButtons[HomeFurnitureController.FurniturePaint.MODEL_MATERIALS] = dialog.findElement("[name='paint-checkbox'][value='MODEL_MATERIALS']");
2439 
2440     for (var paint = 0; paint < colorAndTextureRadioButtons.length; paint++) {
2441       var radioButton = colorAndTextureRadioButtons[paint];
2442       radioButton.checked = paint == selectedPaint 
2443           || (paint == HomeFurnitureController.FurniturePaint.DEFAULT && !colorAndTextureRadioButtons[selectedPaint]);
2444     }
2445 
2446     // Material
2447     var modelMaterialsComponent = controller.getModelMaterialsController().getView();
2448     this.attachChildComponent("material-component", modelMaterialsComponent);
2449 
2450     var uniqueModel = controller.getModelMaterialsController().getModel() != null;
2451     colorAndTextureRadioButtons[HomeFurnitureController.FurniturePaint.MODEL_MATERIALS].disabled = !uniqueModel;
2452     modelMaterialsComponent.setEnabled(uniqueModel);
2453 
2454     this.paintPanel = {
2455         colorAndTextureRadioButtons: colorAndTextureRadioButtons,
2456         colorButton: colorButton,
2457         textureComponent: textureComponent};
2458 
2459     var panelDisplay = controller.isPropertyEditable("PAINT") && controller.isTexturable() ? undefined : "none";
2460     this.getElement("paint-panel").style.display = panelDisplay;
2461     this.getElement("paint-panel").previousElementSibling.style.display = panelDisplay;
2462 
2463     this.registerPropertyChangeListener(controller, "PAINT", function(ev) {
2464         dialog.updatePaintRadioButtons();
2465       });
2466 
2467     this.registerEventListener(colorAndTextureRadioButtons, "change", function(ev) { 
2468         var paint = colorAndTextureRadioButtons.indexOf(ev.target);
2469         controller.setPaint(paint);
2470       });
2471   }
2472 
2473   /**
2474    * @private
2475    */
2476   HomeFurnitureDialog.prototype.updatePaintRadioButtons = function() {
2477     var dialog = this;
2478     var controller = this.controller;
2479     var colorAndTextureRadioButtons = dialog.paintPanel.colorAndTextureRadioButtons;
2480     if (controller.getPaint() == null) {
2481       for (var i = 0; i < colorAndTextureRadioButtons.length; i++) {
2482         colorAndTextureRadioButtons[i].checked = false;
2483       }
2484     } else {
2485       var selectedRadioButton = colorAndTextureRadioButtons[controller.getPaint()];
2486       if (selectedRadioButton) {
2487         selectedRadioButton.checked = true;
2488       }
2489       this.updateShininessRadioButtons(controller);
2490     }
2491   };
2492   
2493   /**
2494    * @private
2495    */
2496   HomeFurnitureDialog.prototype.initSizePanel = function() {
2497     var dialog = this;
2498     var controller = this.controller;
2499   
2500     var widthLabel = this.getElement("width-label");
2501     var widthInput = new JSSpinner(this.preferences, this.getElement("width-input"), 
2502         {
2503           nullable: controller.getWidth() == null,
2504           format: this.preferences.getLengthUnit().getFormat(),
2505           stepSize: this.preferences.getLengthUnit().getStepSize()
2506         });
2507     var depthLabel = this.getElement("depth-label");
2508     var depthInput = new JSSpinner(this.preferences, this.getElement("depth-input"), 
2509         {
2510           nullable: controller.getDepth() == null,
2511           format: this.preferences.getLengthUnit().getFormat(),
2512           stepSize: this.preferences.getLengthUnit().getStepSize()
2513         });
2514     var heightLabel = this.getElement("height-label");
2515     var heightInput = this.getElement("height-input");
2516     var heightInput = new JSSpinner(this.preferences, this.getElement("height-input"), 
2517         {
2518           nullable: controller.getHeight() == null,
2519           format: this.preferences.getLengthUnit().getFormat(),
2520           stepSize: this.preferences.getLengthUnit().getStepSize()
2521         });
2522     var keepProportionsCheckBox = this.getElement("keep-proportions-checkbox");
2523     var modelTransformationsButton = this.getElement("model-transformations-button");
2524 
2525     // 1) Adjust visibility
2526     var widthDisplay = this.controller.isPropertyEditable("WIDTH") ? "initial" : "none";
2527     var depthDisplay = this.controller.isPropertyEditable("DEPTH") ? "initial" : "none";
2528     var heightDisplay = this.controller.isPropertyEditable("HEIGHT") ? "initial" : "none";
2529     var keepProportionsDisplay = this.controller.isPropertyEditable("PROPORTIONAL") ? "initial" : "none";
2530 
2531     widthLabel.style.display = widthDisplay;
2532     widthInput.parentElement.style.display = widthDisplay;
2533     depthLabel.style.display = depthDisplay;
2534     depthInput.parentElement.style.display = depthDisplay;
2535     heightLabel.style.display = heightDisplay;
2536     heightInput.parentElement.style.display = heightDisplay;
2537     keepProportionsCheckBox.parentElement.style.display = keepProportionsDisplay;
2538     modelTransformationsButton.parentElement.style.display = "none";
2539     
2540     if (this.controller.isPropertyEditable("MODEL_TRANSFORMATIONS")
2541         && modelTransformationsButton != null) {
2542       var modelMaterialsController = controller.getModelMaterialsController();
2543       if (modelMaterialsController !== null && modelMaterialsController.getModel() !== null) {
2544         ModelManager.getInstance().loadModel(modelMaterialsController.getModel(),
2545             {
2546               modelUpdated : function(modelRoot) {
2547                 var modelManager = ModelManager.getInstance();
2548                 if (modelManager.containsDeformableNode(modelRoot)) {
2549                   // Make button visible only if model contains some deformable nodes
2550                   modelTransformationsButton.innerHTML = dialog.getLocalizedLabelText("HomeFurniturePanel", 
2551                       modelManager.containsNode(modelRoot, ModelManager.MANNEQUIN_ABDOMEN_PREFIX)
2552                           ? "mannequinTransformationsButton.text"
2553                           : "modelTransformationsButton.text");
2554                   modelTransformationsButton.parentElement.style.display = "block";
2555                   dialog.registerEventListener(modelTransformationsButton, "click", function(ev) {
2556                       dialog.displayModelTransformationsView(dialog.getUserPreferences(), dialog.controller);
2557                     });
2558                 }
2559               },
2560               modelError : function(ex) {
2561                 // Ignore missing models
2562               }
2563             });
2564       }
2565     }
2566     
2567     // 2) Set values
2568     widthInput.setValue(this.controller.getWidth());
2569     depthInput.setValue(this.controller.getDepth());
2570     heightInput.setValue(this.controller.getHeight());
2571     keepProportionsCheckBox.checked = this.controller.isProportional();
2572 
2573     // 3) Set labels
2574     var unitName = this.preferences.getLengthUnit().getName();
2575     widthLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "widthLabel.text", unitName);
2576     depthLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "depthLabel.text", unitName);
2577     heightLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "heightLabel.text", unitName);
2578 
2579     // 4) Set custom attributes
2580     var minimumLength = this.preferences.getLengthUnit().getMinimumLength();
2581     var maximumLength = this.preferences.getLengthUnit().getMaximumLength();
2582     widthInput.setMinimum(minimumLength);
2583     widthInput.setMaximum(maximumLength); 
2584     depthInput.setMinimum(minimumLength); 
2585     depthInput.setMaximum(maximumLength); 
2586     heightInput.setMinimum(minimumLength);
2587     heightInput.setMaximum(maximumLength);
2588 
2589     // 5) Add property listeners
2590     var controller = this.controller;
2591     this.registerPropertyChangeListener(this.controller, "WIDTH", function(ev) {
2592         widthInput.setValue(controller.getWidth());
2593       });
2594     this.registerPropertyChangeListener(this.controller, "DEPTH", function(ev) {
2595         depthInput.setValue(controller.getDepth());
2596       });
2597     this.registerPropertyChangeListener(this.controller, "HEIGHT", function(ev) {
2598         heightInput.setValue(controller.getHeight());
2599       });
2600     this.registerPropertyChangeListener(this.controller, "PROPORTIONAL", function(ev) {
2601         keepProportionsCheckBox.checked = controller.isProportional();
2602       });
2603 
2604     // 6) Add change listeners
2605     this.registerEventListener(widthInput, "input", function(ev) {
2606           controller.setWidth(widthInput.getValue());
2607         });
2608     this.registerEventListener(depthInput, "input", function(ev) {
2609           controller.setDepth(depthInput.getValue());
2610         });
2611     this.registerEventListener(heightInput, "input", function(ev) {
2612           controller.setHeight(heightInput.getValue());
2613         });
2614     this.registerEventListener(keepProportionsCheckBox, "change", function(ev) {
2615           controller.setProportional(keepProportionsCheckBox.checked);
2616         });
2617 
2618     // 7) Assign panel's components
2619     this.sizePanel = {
2620         widthLabel: widthLabel,
2621         widthInput: widthInput,
2622         depthLabel: depthLabel,
2623         depthInput: depthInput,
2624         heightLabel: heightLabel,
2625         heightInput: heightInput,
2626         keepProportionsCheckBox: keepProportionsCheckBox
2627       };
2628 
2629     // 8) Handle components activation
2630     this.updateSizeComponents();
2631     // Add a listener that enables / disables size fields depending on furniture
2632     // resizable and deformable
2633     this.registerPropertyChangeListener(this.controller, "RESIZABLE", function(ev) {
2634         dialog.updateSizeComponents();
2635       });
2636     this.registerPropertyChangeListener(this.controller, "DEFORMABLE", function(ev) {
2637         dialog.updateSizeComponents();
2638       });
2639   };
2640 
2641   /**
2642    * @private
2643    */
2644   HomeFurnitureDialog.prototype.updateSizeComponents = function() {
2645     var editableSize = this.controller.isResizable();
2646     this.sizePanel.widthLabel.disabled = !editableSize;
2647     this.sizePanel.widthInput.disabled = !editableSize;
2648     this.sizePanel.depthLabel.disabled = !editableSize;
2649     this.sizePanel.depthInput.disabled = !editableSize;
2650     this.sizePanel.heightLabel.disabled = !editableSize;
2651     this.sizePanel.heightInput.disabled = !editableSize;
2652     this.sizePanel.keepProportionsCheckBox.disabled = !editableSize || !this.controller.isDeformable();
2653   };
2654 
2655   /**
2656    * @private
2657    */
2658   HomeFurnitureDialog.prototype.initShininessPanel = function() {
2659     var controller = this.controller;
2660     var dialog = this;
2661 
2662     var defaultShininessRadioButton = this.findElement("[name='shininess-radio'][value='DEFAULT']");
2663     var mattRadioButton = this.findElement("[name='shininess-radio'][value='MATT']");
2664     var shinyRadioButton = this.findElement("[name='shininess-radio'][value='SHINY']");
2665     this.shininessPanel = {
2666         defaultShininessRadioButton: defaultShininessRadioButton,
2667         mattRadioButton: mattRadioButton,
2668         shinyRadioButton: shinyRadioButton};
2669 
2670     var panelDisplay = controller.isPropertyEditable("SHININESS") && controller.isTexturable() ? undefined : "none";
2671     this.getElement("shininess-panel").style.display = panelDisplay;
2672     this.getElement("shininess-panel").previousElementSibling.style.display = panelDisplay;
2673 
2674     if (controller.isPropertyEditable("SHININESS")) {
2675       // Create radio buttons bound to SHININESS controller properties
2676       this.registerEventListener([defaultShininessRadioButton, mattRadioButton, shinyRadioButton], "change",
2677           function(ev) {
2678             var selectedRadioButton = dialog.findElement("[name='shininess-radio']:checked");
2679             controller.setShininess(HomeFurnitureController.FurnitureShininess[selectedRadioButton.value]);
2680           });
2681       this.registerPropertyChangeListener(controller, "SHININESS", function(ev) {
2682           dialog.updateShininessRadioButtons(controller);
2683         });
2684       
2685       this.updateShininessRadioButtons(controller);
2686     }
2687   };
2688 
2689   /**
2690    * @private
2691    */
2692   HomeFurnitureDialog.prototype.updateShininessRadioButtons = function() {
2693     var controller = this.controller;
2694 
2695     if (controller.isPropertyEditable("SHININESS")) {
2696       if (controller.getShininess() == HomeFurnitureController.FurnitureShininess.DEFAULT) {
2697         this.shininessPanel.defaultShininessRadioButton.checked = true;
2698       } else if (controller.getShininess() == HomeFurnitureController.FurnitureShininess.MATT) {
2699         this.shininessPanel.mattRadioButton.checked = true;
2700       } else if (controller.getShininess() == HomeFurnitureController.FurnitureShininess.SHINY) {
2701         this.shininessPanel.shinyRadioButton.checked = true;
2702       } else { // null
2703         this.shininessPanel.defaultShininessRadioButton.checked = false;
2704         this.shininessPanel.mattRadioButton.checked = false;
2705         this.shininessPanel.shinyRadioButton.checked = false;
2706       }
2707       var shininessEnabled = controller.getPaint() != HomeFurnitureController.FurniturePaint.MODEL_MATERIALS;
2708       this.shininessPanel.defaultShininessRadioButton.disabled = !shininessEnabled;
2709       this.shininessPanel.mattRadioButton.disabled = !shininessEnabled;
2710       this.shininessPanel.shinyRadioButton.disabled = !shininessEnabled;
2711       if (!shininessEnabled) {
2712         this.shininessPanel.defaultShininessRadioButton.checked = false;
2713         this.shininessPanel.mattRadioButton.checked = false;
2714         this.shininessPanel.shinyRadioButton.checked = false;
2715       }
2716     }
2717   };
2718 
2719   /**
2720    * @private
2721    */
2722   HomeFurnitureDialog.prototype.displayModelTransformationsView = function(preferences, controller) {
2723     var html = 
2724         "<div data-name='label-panel'>"
2725       + "  <span>@{ModelTransformationsPanel.transformationsLabel.text}</span><br/>"
2726       + "</div>"
2727       + "<div data-name='preview-panel'>" 
2728       + "  <canvas id='model-preview-canvas'></canvas>" 
2729       + "</div>"
2730       + "<div data-name='edit-panel'>" 
2731       + "  <div>"
2732       + "    <button name='reset-transformations-button'>@{ModelTransformationsPanel.resetTransformationsButton.text}</button>" 
2733       + "    <div name='presetTransformationsLabel'>@{ModelTransformationsPanel.presetTransformationsLabel.text}</div> "
2734       + "    <div>" 
2735       + "      <select name='presetTransformations'></select>"
2736       + "    </div>"
2737       + "    <button name='view-from-front-button'>@{ModelTransformationsPanel.viewFromFrontButton.text}</button>"
2738       + "    <button name='view-from-side-button'>@{ModelTransformationsPanel.viewFromSideButton.text}</button>"
2739       + "    <button name='view-from-top-button'>@{ModelTransformationsPanel.viewFromTopButton.text}</button>"
2740       + "  </div>"
2741       + "</div>";
2742     var dialog = new JSDialog(preferences, 
2743         preferences.getLocalizedString("ModelTransformationsPanel", "modelTransformations.title"),
2744         html, 
2745         {
2746           size: "medium",
2747           applier: function() {
2748             var modelX = controller.getModelMirrored()
2749                 ? -dialog.previewComponent.getModelX()
2750                 : dialog.previewComponent.getModelX();
2751             var modelY = dialog.previewComponent.getModelY();
2752             var pieceX = (controller.getX()
2753                 + modelX * Math.cos(controller.getAngle()) - modelY * Math.sin(controller.getAngle()));
2754             var pieceY = (controller.getY()
2755                 + modelX * Math.sin(controller.getAngle()) + modelY * Math.cos(controller.getAngle()));
2756             var pieceElevation = controller.getElevation()
2757                 + dialog.previewComponent.getModelElevation() + controller.getHeight() / 2;
2758             var modelTransformations = dialog.previewComponent.getModelTransformations();
2759             controller.setModelTransformations(modelTransformations !== null ? modelTransformations : [],
2760                 pieceX, pieceY, pieceElevation,
2761                 dialog.previewComponent.getModelWidth(),
2762                 dialog.previewComponent.getModelDepth(),
2763                 dialog.previewComponent.getModelHeight());
2764           },
2765           disposer: function() {            
2766           }
2767         });
2768 
2769     dialog.getHTMLElement().classList.add("model-transformations-chooser-dialog");
2770 
2771     var previewCanvas = dialog.findElement("#model-preview-canvas");
2772     previewCanvas.width = 350;
2773     previewCanvas.height = 350;
2774     
2775     var modelMaterialsController = controller.getModelMaterialsController();
2776     dialog.previewComponent = new ModelPreviewComponent(previewCanvas, true, true);
2777     dialog.previewComponent.setModel(modelMaterialsController.getModel(), modelMaterialsController.getModelFlags(), modelMaterialsController.getModelRotation(),
2778         modelMaterialsController.getModelWidth(), modelMaterialsController.getModelDepth(), modelMaterialsController.getModelHeight());
2779     dialog.previewComponent.setModelMaterials(modelMaterialsController.getMaterials());
2780     dialog.previewComponent.setModelTransformations(controller.getModelTransformations());
2781     
2782     var resetTransformationsButton = dialog.getElement("reset-transformations-button");
2783     dialog.registerEventListener(resetTransformationsButton, "click", function(ev) {
2784         dialog.previewComponent.resetModelTransformations();
2785       });
2786     var presetTransformationsSelect = dialog.getElement("presetTransformations");
2787     var modelPresetTransformationsNames = controller.getModelPresetTransformationsNames();
2788     if (modelPresetTransformationsNames.length > 0) {
2789       var option = document.createElement("option");
2790       option.value = -1;
2791       option.textContent = preferences.getLocalizedString("ModelTransformationsPanel", "presetTransformationsComboBox.chooseTransformations.text");
2792       presetTransformationsSelect.appendChild(option);    
2793       for (var i = 0; i < modelPresetTransformationsNames.length; i++) {
2794         var option = document.createElement("option");
2795         option.value = i;
2796         option.textContent = modelPresetTransformationsNames[i];
2797         presetTransformationsSelect.appendChild(option);
2798       }
2799       dialog.registerEventListener(presetTransformationsSelect, "change", function(ev) {
2800           if (presetTransformationsSelect.value >= 0)
2801             dialog.previewComponent.setPresetModelTransformations(
2802               controller.getModelPresetTransformations(presetTransformationsSelect.value));
2803         });
2804     } else {
2805       dialog.getElement("presetTransformationsLabel").style.display = "none";
2806       presetTransformationsSelect.style.display = "none";
2807     }
2808     var viewFromFrontButton = dialog.getElement("view-from-front-button");
2809     dialog.registerEventListener(viewFromFrontButton, "click", function(ev) {
2810         dialog.previewComponent.setViewYaw(0);
2811         dialog.previewComponent.setViewPitch(0);
2812       });
2813     var viewFromSideButton = dialog.getElement("view-from-side-button");
2814     dialog.registerEventListener(viewFromSideButton, "click", function(ev) {
2815         dialog.previewComponent.setViewYaw(Math.PI / 2);
2816         dialog.previewComponent.setViewPitch(0);
2817       });
2818     var viewFromTopButton = dialog.getElement("view-from-top-button");
2819     dialog.registerEventListener(viewFromTopButton, "click", function(ev) {
2820         dialog.previewComponent.setViewYaw(0);
2821         dialog.previewComponent.setViewPitch(-Math.PI / 2);
2822       });
2823 
2824     dialog.displayView();
2825   };
2826 
2827   return new HomeFurnitureDialog();
2828 }
2829 
2830 JSViewFactory.prototype.createWallView = function(preferences, controller) {
2831   var initStartAndEndPointsPanel = function(dialog) {
2832     var maximumLength = preferences.getLengthUnit().getMaximumLength();
2833 
2834     var xStartLabel = dialog.getElement("x-start-label");
2835     var yStartLabel = dialog.getElement("y-start-label");
2836     var xStartInput = new JSSpinner(preferences, dialog.getElement("x-start-input"), 
2837         {
2838           nullable: controller.getXStart() == null,
2839           format: preferences.getLengthUnit().getFormat(),
2840           value: controller.getXStart(),
2841           minimum: -maximumLength,
2842           maximum: maximumLength,
2843           stepSize: preferences.getLengthUnit().getStepSize()
2844         });
2845     var yStartInput = new JSSpinner(preferences, dialog.getElement("y-start-input"), 
2846         {
2847           nullable: controller.getYStart() == null,
2848           format: preferences.getLengthUnit().getFormat(),
2849           value: controller.getYStart(),
2850           minimum: -maximumLength,
2851           maximum: maximumLength,
2852           stepSize: preferences.getLengthUnit().getStepSize()
2853         });
2854     var xEndLabel = dialog.getElement("x-end-label");
2855     var yEndLabel = dialog.getElement("y-end-label");
2856     var distanceToEndPointLabel = dialog.getElement("distance-to-end-point-label");
2857     var xEndInput = new JSSpinner(preferences, dialog.getElement("x-end-input"), 
2858         {
2859           nullable: controller.getXEnd() == null,
2860           format: preferences.getLengthUnit().getFormat(),
2861           value: controller.getXEnd(),
2862           minimum: -maximumLength,
2863           maximum: maximumLength,
2864           stepSize: preferences.getLengthUnit().getStepSize()
2865         });
2866     var yEndInput = new JSSpinner(preferences, dialog.getElement("y-end-input"), 
2867         {
2868           nullable: controller.getYEnd() == null,
2869           format: preferences.getLengthUnit().getFormat(),
2870           value: controller.getYEnd(),
2871           minimum: -maximumLength,
2872           maximum: maximumLength,
2873           stepSize: preferences.getLengthUnit().getStepSize()
2874         });
2875     var distanceToEndPointInput = new JSSpinner(preferences, dialog.getElement("distance-to-end-point-input"), 
2876         {
2877           nullable: controller.getDistanceToEndPoint() == null,
2878           format: preferences.getLengthUnit().getFormat(),
2879           value: controller.getDistanceToEndPoint(),
2880           minimum: preferences.getLengthUnit().getMinimumLength(),
2881           maximum: 2 * maximumLength * Math.sqrt(2),
2882           stepSize: preferences.getLengthUnit().getStepSize()
2883         });
2884 
2885     var unitName = preferences.getLengthUnit().getName();
2886     xStartLabel.textContent = dialog.getLocalizedLabelText("WallPanel", "xLabel.text", unitName);
2887     xEndLabel.textContent = dialog.getLocalizedLabelText("WallPanel", "xLabel.text", unitName);
2888     yStartLabel.textContent = dialog.getLocalizedLabelText("WallPanel", "yLabel.text", unitName);
2889     yEndLabel.textContent = dialog.getLocalizedLabelText("WallPanel", "yLabel.text", unitName);
2890     distanceToEndPointLabel.textContent = dialog.getLocalizedLabelText("WallPanel", "distanceToEndPointLabel.text", unitName);
2891 
2892     dialog.registerPropertyChangeListener(controller, "X_START", function(ev) {
2893         xStartInput.setValue(ev.getNewValue());
2894       });
2895     dialog.registerPropertyChangeListener(controller, "Y_START", function(ev) {
2896         yStartInput.setValue(ev.getNewValue());
2897       });
2898     dialog.registerPropertyChangeListener(controller, "X_END", function(ev) {
2899         xEndInput.setValue(ev.getNewValue());
2900       });
2901     dialog.registerPropertyChangeListener(controller, "Y_END", function(ev) {
2902         yEndInput.setValue(ev.getNewValue());
2903       });
2904     dialog.registerPropertyChangeListener(controller, "DISTANCE_TO_END_POINT", function(ev) {
2905         distanceToEndPointInput.setValue(ev.getNewValue());
2906       });
2907 
2908     dialog.registerEventListener(xStartInput, "input", function(ev) {
2909         controller.setXStart(xStartInput.getValue());
2910       });
2911     dialog.registerEventListener(yStartInput, "input", function(ev) {
2912         controller.setYStart(yStartInput.getValue());
2913       });
2914     dialog.registerEventListener(xEndInput, "input", function(ev) {
2915         controller.setXEnd(xEndInput.getValue());
2916       });
2917     dialog.registerEventListener(yEndInput, "input", function(ev) {
2918         controller.setYEnd(yEndInput.getValue());
2919       });
2920     dialog.registerEventListener(distanceToEndPointInput, "input", function(ev) {
2921         controller.setDistanceToEndPoint(distanceToEndPointInput.getValue());
2922       });
2923   }
2924 
2925   var editBaseboard = function(dialogTitle, controller) {
2926       var view = controller.getView();
2927       var dialog = new JSDialog(preferences, dialogTitle,
2928           "<div data-name='content'></div>", 
2929           {
2930             size: "small",
2931             applier: function() {
2932               // Do not remove - applier must be defined so OK button shows
2933             },
2934             disposer: function() {
2935               view.dispose();
2936             }
2937           });
2938       dialog.attachChildComponent("content", view);
2939   
2940       var visible = controller.getVisible();
2941       var color = controller.getColor();
2942       var texture = controller.getTextureController().getTexture();
2943       var paint = controller.getPaint();
2944       var thickness = controller.getThickness();
2945       var height = controller.getHeight();
2946   
2947       dialog.cancel = function() {
2948           controller.setVisible(visible);
2949           controller.setColor(color);
2950           controller.getTextureController().setTexture(texture);
2951           controller.setPaint(paint);
2952           controller.setThickness(thickness);
2953           controller.setHeight(height);
2954           dialog.close();
2955         };
2956       dialog.displayView();
2957     };
2958 
2959   var initLeftAndRightSidesPanels = function(dialog) {
2960     var leftSideColorRadioButton = dialog.findElement("[name='left-side-color-and-texture-choice'][value='COLORED']");
2961     var leftSideTextureRadioButton = dialog.findElement("[name='left-side-color-and-texture-choice'][value='TEXTURED']");
2962     var rightSideColorRadioButton = dialog.findElement("[name='right-side-color-and-texture-choice'][value='COLORED']");
2963     var rightSideTextureRadioButton = dialog.findElement("[name='right-side-color-and-texture-choice'][value='TEXTURED']");
2964 
2965     var updateLeftSidePaint = function() {
2966       leftSideColorRadioButton.checked = controller.getLeftSidePaint() == WallController.WallPaint.COLORED;
2967       leftSideTextureRadioButton.checked = controller.getLeftSidePaint() == WallController.WallPaint.TEXTURED;
2968     }
2969     var updateRightSidePaint = function() {
2970       rightSideColorRadioButton.checked = controller.getRightSidePaint() == WallController.WallPaint.COLORED;
2971       rightSideTextureRadioButton.checked = controller.getRightSidePaint() == WallController.WallPaint.TEXTURED;
2972     }
2973 
2974     updateLeftSidePaint();
2975     updateRightSidePaint();
2976     dialog.registerPropertyChangeListener(controller, "LEFT_SIDE_PAINT", function() {
2977         updateLeftSidePaint();
2978       });
2979     dialog.registerPropertyChangeListener(controller, "RIGHT_SIDE_PAINT", function() {
2980         updateRightSidePaint();
2981       });
2982 
2983     // Colors
2984     dialog.leftSideColorButton = new ColorButton(preferences,  
2985         {
2986           colorChanged: function(selectedColor) {
2987               dialog.findElement("[name='left-side-color-and-texture-choice'][value='COLORED']").checked = true;
2988               controller.setLeftSidePaint(WallController.WallPaint.COLORED);
2989               controller.setLeftSideColor(selectedColor);
2990             }
2991         });
2992     dialog.rightSideColorButton = new ColorButton(preferences,  
2993         {
2994           colorChanged: function(selectedColor) {
2995             dialog.findElement("[name='right-side-color-and-texture-choice'][value='COLORED']").checked = true;
2996             controller.setRightSidePaint(WallController.WallPaint.COLORED);
2997             controller.setRightSideColor(selectedColor);
2998           }
2999         });
3000     dialog.attachChildComponent("left-side-color-button", dialog.leftSideColorButton);
3001     dialog.attachChildComponent("right-side-color-button", dialog.rightSideColorButton);
3002 
3003     dialog.leftSideColorButton.setColor(controller.getLeftSideColor());
3004     dialog.leftSideColorButton.setColorDialogTitle(preferences.getLocalizedString(
3005         "WallPanel", "leftSideColorDialog.title"));
3006     dialog.rightSideColorButton.setColor(controller.getRightSideColor());
3007     dialog.rightSideColorButton.setColorDialogTitle(preferences.getLocalizedString(
3008         "WallPanel", "rightSideColorDialog.title"));
3009     dialog.registerPropertyChangeListener(controller, "LEFT_SIDE_COLOR", function() {
3010         dialog.leftSideColorButton.setColor(controller.getLeftSideColor());
3011       });
3012     dialog.registerPropertyChangeListener(controller, "RIGHT_SIDE_COLOR", function() {
3013         dialog.rightSideColorButton.setColor(controller.getRightSideColor());
3014       });
3015 
3016     // Textures
3017     dialog.leftSideTextureComponent = controller.getLeftSideTextureController().getView();
3018     dialog.attachChildComponent('left-side-texture-component', dialog.leftSideTextureComponent);
3019 
3020     dialog.rightSideTextureComponent = controller.getRightSideTextureController().getView();
3021     dialog.attachChildComponent("right-side-texture-component", dialog.rightSideTextureComponent);
3022 
3023     // Shininess
3024     var leftSideMattRadioButton = dialog.findElement("[name='left-side-shininess-choice'][value='0']");
3025     var leftSideShinyRadioButton = dialog.findElement("[name='left-side-shininess-choice'][value='0.25']");
3026     var rightSideMattRadioButton = dialog.findElement("[name='right-side-shininess-choice'][value='0']");
3027     var rightSideShinyRadioButton = dialog.findElement("[name='right-side-shininess-choice'][value='0.25']");
3028 
3029     var updateLeftSideShininess = function() {
3030       leftSideMattRadioButton.checked = controller.getLeftSideShininess() == 0;
3031       leftSideShinyRadioButton.checked = controller.getLeftSideShininess() == 0.25;
3032     }
3033     var updateRightSideShininess = function() {
3034       rightSideMattRadioButton.checked = controller.getRightSideShininess() == 0;
3035       rightSideShinyRadioButton.checked = controller.getRightSideShininess() == 0.25
3036     }
3037 
3038     updateLeftSideShininess();
3039     updateRightSideShininess();
3040     dialog.registerPropertyChangeListener(controller, "LEFT_SIDE_SHININESS", function(ev) {
3041         updateLeftSideShininess();
3042       });
3043     dialog.registerPropertyChangeListener(controller, "RIGHT_SIDE_SHININESS", function(ev) {
3044         updateRightSideShininess();
3045       });
3046     dialog.registerEventListener([leftSideMattRadioButton, leftSideShinyRadioButton], "change", 
3047         function(ev) {
3048           controller.setLeftSideShininess(parseFloat(this.value));
3049         });
3050     dialog.registerEventListener([rightSideMattRadioButton, rightSideShinyRadioButton], "change", 
3051         function(ev) {
3052           controller.setRightSideShininess(parseFloat(this.value));
3053         });
3054 
3055     // Baseboards
3056     var leftSideBaseboardButton = dialog.getElement("left-side-modify-baseboard-button");
3057     var rightSideBaseboardButton = dialog.getElement("right-side-modify-baseboard-button");
3058     var leftSideBaseboardButtonAction = new ResourceAction(preferences, "WallPanel", "MODIFY_LEFT_SIDE_BASEBOARD", true);
3059     var rightSideBaseboardButtonAction = new ResourceAction(preferences, "WallPanel", "MODIFY_RIGHT_SIDE_BASEBOARD", true);
3060     leftSideBaseboardButton.textContent = leftSideBaseboardButtonAction.getValue(AbstractAction.NAME);
3061     rightSideBaseboardButton.textContent = rightSideBaseboardButtonAction.getValue(AbstractAction.NAME);
3062 
3063     dialog.registerEventListener(leftSideBaseboardButton, "click", function(ev) {
3064         editBaseboard(dialog.getLocalizedLabelText("WallPanel", "leftSideBaseboardDialog.title"), 
3065             controller.getLeftSideBaseboardController());
3066       });
3067     dialog.registerEventListener(rightSideBaseboardButton, "click", function(ev) {
3068         editBaseboard(dialog.getLocalizedLabelText("WallPanel", "rightSideBaseboardDialog.title"), 
3069             controller.getRightSideBaseboardController());
3070       });
3071   };
3072 
3073   var initTopPanel = function(dialog) {
3074       var patternsTexturesByURL = {};
3075       var patterns = preferences.getPatternsCatalog().getPatterns();
3076       for (var i = 0; i < patterns.length; i++) {
3077         var url = patterns[i].getImage().getURL();
3078         patternsTexturesByURL[url] = patterns[i];
3079       }
3080       var patternSelect = dialog.getElement("pattern-select");
3081       patternSelect.classList.add("wall-pattern-combo-box");
3082       var patternComboBox = new JSComboBox(this.preferences, patternSelect, 
3083           {
3084             nullable: controller.getPattern() != null,
3085             availableValues: Object.keys(patternsTexturesByURL),
3086             renderCell: function(patternURL, patternItemElement) {
3087               patternItemElement.style.backgroundImage = "url('" + patternURL + "')";
3088             },
3089             selectionChanged: function(newValue) {
3090               controller.setPattern(patternsTexturesByURL[newValue]);
3091             }
3092           });
3093   
3094       var patternChangeListener = function() {
3095           var pattern = controller.getPattern();
3096           patternComboBox.setSelectedItem(controller.getPattern() != null
3097               ? pattern.getImage().getURL()
3098               : null);
3099         };
3100       patternChangeListener();
3101       dialog.registerPropertyChangeListener(controller, "PATTERN", function(ev) {
3102           patternChangeListener();
3103         });
3104   
3105       var topDefaultColorRadioButton = dialog.findElement("[name='top-color-choice'][value='DEFAULT']");
3106       var topColorRadioButton = dialog.findElement("[name='top-color-choice'][value='COLORED']");
3107       var topPaintRadioButtons = [topDefaultColorRadioButton, topColorRadioButton];
3108       var topPaintChangeListener = function() {
3109           topDefaultColorRadioButton.checked = controller.getTopPaint() == WallController.WallPaint.DEFAULT;
3110           topColorRadioButton.checked = controller.getTopPaint() == WallController.WallPaint.COLORED;
3111         };
3112   
3113       dialog.registerEventListener(topPaintRadioButtons, "click", function(ev) {
3114           controller.setTopPaint(WallController.WallPaint[this.value]);
3115         });
3116       topPaintChangeListener();
3117       dialog.registerPropertyChangeListener(controller, "TOP_PAINT", function(ev) {
3118           topPaintChangeListener();
3119         });
3120   
3121       dialog.topColorButton = new ColorButton(preferences, 
3122           {
3123             colorChanged: function(selectedColor) {
3124               topColorRadioButton.checked = true;
3125               controller.setTopPaint(WallController.WallPaint.COLORED);
3126               controller.setTopColor(selectedColor);
3127             }
3128           });
3129       dialog.attachChildComponent("top-color-button", dialog.topColorButton);
3130       dialog.topColorButton.setColor(controller.getTopColor());
3131       dialog.topColorButton.setColorDialogTitle(preferences.getLocalizedString(
3132           "WallPanel", "topColorDialog.title"));
3133       dialog.registerPropertyChangeListener(controller, "TOP_COLOR", function() {
3134           dialog.topColorButton.setColor(controller.getTopColor());
3135         });
3136     };
3137 
3138   var initHeightPanel = function(dialog) {
3139       var unitName = preferences.getLengthUnit().getName();
3140       dialog.getElement("rectangular-wall-height-label").textContent = dialog.getLocalizedLabelText("WallPanel", "rectangularWallHeightLabel.text", unitName);
3141   
3142       var rectangularWallRadioButton = dialog.findElement("[name='wall-shape-choice'][value='RECTANGULAR_WALL']");
3143       var slopingWallRadioButton = dialog.findElement("[name='wall-shape-choice'][value='SLOPING_WALL']");
3144   
3145       dialog.registerEventListener([rectangularWallRadioButton, slopingWallRadioButton], "change", function(ev) {
3146           controller.setShape(WallController.WallShape[this.value]);
3147         });
3148       var wallShapeChangeListener = function() {
3149           rectangularWallRadioButton.checked = controller.getShape() == WallController.WallShape.RECTANGULAR_WALL;
3150           slopingWallRadioButton.checked = controller.getShape() == WallController.WallShape.SLOPING_WALL;
3151         };
3152       wallShapeChangeListener();
3153       dialog.registerPropertyChangeListener(controller, "SHAPE", function(ev) {
3154           wallShapeChangeListener();
3155         });
3156   
3157       var minimumLength = preferences.getLengthUnit().getMinimumLength();
3158       var maximumLength = preferences.getLengthUnit().getMaximumLength();
3159       var rectangularWallHeightInput = new JSSpinner(preferences, dialog.getElement("rectangular-wall-height-input"), 
3160           {
3161             nullable: controller.getRectangularWallHeight() == null,
3162             format: preferences.getLengthUnit().getFormat(),
3163             value: controller.getRectangularWallHeight(),
3164             minimum: minimumLength,
3165             maximum: maximumLength,
3166             stepSize: preferences.getLengthUnit().getStepSize()
3167           });
3168       dialog.registerPropertyChangeListener(controller, "RECTANGULAR_WALL_HEIGHT", function(ev) {
3169           rectangularWallHeightInput.setValue(ev.getNewValue());
3170         });
3171       dialog.registerEventListener(rectangularWallHeightInput, "input", function(ev) {
3172           controller.setRectangularWallHeight(rectangularWallHeightInput.getValue());
3173         });
3174   
3175       var minimumHeight = controller.getSlopingWallHeightAtStart() != null && controller.getSlopingWallHeightAtEnd() != null
3176           ? 0
3177           : minimumLength;
3178       var slopingWallHeightAtStartInput = new JSSpinner(preferences, dialog.getElement("sloping-wall-height-at-start-input"), 
3179           {
3180             nullable: controller.getSlopingWallHeightAtStart() == null,
3181             format: preferences.getLengthUnit().getFormat(),
3182             value: controller.getSlopingWallHeightAtStart(),
3183             minimum: minimumHeight,
3184             maximum: maximumLength,
3185             stepSize: preferences.getLengthUnit().getStepSize()
3186           });
3187       dialog.registerPropertyChangeListener(controller, "SLOPING_WALL_HEIGHT_AT_START", function(ev) {
3188           slopingWallHeightAtStartInput.setValue(ev.getNewValue());
3189         });
3190       dialog.registerEventListener(slopingWallHeightAtStartInput, "input", function(ev) {
3191           controller.setSlopingWallHeightAtStart(slopingWallHeightAtStartInput.getValue());
3192           if (minimumHeight == 0
3193               && controller.getSlopingWallHeightAtStart() == 0
3194               && controller.getSlopingWallHeightAtEnd() == 0) {
3195             // Ensure wall height is never 0
3196             controller.setSlopingWallHeightAtEnd(minimumLength);
3197           }
3198         });
3199   
3200       var slopingWallHeightAtEndInput = new JSSpinner(preferences, dialog.getElement("sloping-wall-height-at-end-input"), 
3201           {
3202             nullable: controller.getSlopingWallHeightAtEnd() == null,
3203             format: preferences.getLengthUnit().getFormat(),
3204             value: controller.getSlopingWallHeightAtEnd(),
3205             minimum: minimumHeight,
3206             maximum: maximumLength,
3207             stepSize: preferences.getLengthUnit().getStepSize()
3208           });
3209       dialog.registerPropertyChangeListener(controller, "SLOPING_WALL_HEIGHT_AT_END", function(ev) {
3210           slopingWallHeightAtEndInput.setValue(ev.getNewValue());
3211         });
3212       dialog.registerEventListener(slopingWallHeightAtEndInput, "input", function(ev) {
3213           controller.setSlopingWallHeightAtEnd(slopingWallHeightAtEndInput.getValue());
3214           if (minimumHeight == 0
3215               && controller.getSlopingWallHeightAtStart() == 0
3216               && controller.getSlopingWallHeightAtEnd() == 0) {
3217             // Ensure wall height is never 0
3218             controller.setSlopingWallHeightAtStart(minimumLength);
3219           }
3220         });
3221   
3222       dialog.getElement("thickness-label").textContent = dialog.getLocalizedLabelText("WallPanel", "thicknessLabel.text", unitName);
3223       var thicknessInput = new JSSpinner(preferences, dialog.getElement("thickness-input"), 
3224           {
3225             nullable: controller.getThickness() == null,
3226             format: preferences.getLengthUnit().getFormat(),
3227             value: controller.getThickness(),
3228             minimum: minimumLength,
3229             maximum: maximumLength / 10,
3230             stepSize: preferences.getLengthUnit().getStepSize()
3231           });
3232       dialog.registerPropertyChangeListener(controller, "THICKNESS", function(ev) {
3233           thicknessInput.setValue(ev.getNewValue());
3234         });
3235       dialog.registerEventListener(thicknessInput, "input", function(ev) {
3236           controller.setThickness(thicknessInput.getValue());
3237         });
3238   
3239       dialog.getElement("arc-extent-label").textContent = dialog.getLocalizedLabelText("WallPanel", "arcExtentLabel.text", unitName);
3240       var angleDecimalFormat = new DecimalFormat("0.#");
3241       var arcExtentInput = new JSSpinner(this.preferences, dialog.getElement("arc-extent-input"), 
3242           {
3243             nullable: controller.getArcExtentInDegrees() == null,
3244             format: angleDecimalFormat,
3245             value: 0,
3246             minimum: -270,
3247             maximum: 270,
3248             stepSize: 5
3249           });
3250       var arcExtentChangeListener = function() {
3251           arcExtentInput.setValue(controller.getArcExtentInDegrees());
3252         };
3253       arcExtentChangeListener();
3254       dialog.registerPropertyChangeListener(controller, "ARC_EXTENT_IN_DEGREES", function(ev) {
3255           arcExtentChangeListener();
3256         });
3257   
3258       dialog.registerEventListener(arcExtentInput, "input", function(ev) {
3259           controller.setArcExtentInDegrees(arcExtentInput.getValue());
3260         });
3261     };
3262 
3263   var dialog = new JSDialog(preferences,
3264       "@{WallPanel.wall.title}", 
3265       document.getElementById("wall-dialog-template"), 
3266       {
3267         applier: function(dialog) {
3268           controller.modifyWalls();
3269         },
3270         disposer: function(dialog) {
3271           dialog.leftSideColorButton.dispose();
3272           dialog.rightSideColorButton.dispose();
3273           dialog.leftSideTextureComponent.dispose();
3274           dialog.rightSideTextureComponent.dispose();
3275           dialog.topColorButton.dispose();
3276         },
3277         size: "medium"
3278       });
3279   
3280   initStartAndEndPointsPanel(dialog);
3281   initLeftAndRightSidesPanels(dialog);
3282   initTopPanel(dialog);
3283   initHeightPanel(dialog);
3284 
3285   dialog.getElement("wall-orientation-label").innerHTML = dialog.getLocalizedLabelText(
3286       "WallPanel", "wallOrientationLabel.text", ZIPTools.getScriptFolder() + "resources/wallOrientation.png");
3287 
3288   var setVisible = function(element, visible, parent) {
3289       if (element != null) {
3290         if (parent) {
3291           element = element.parentElement;
3292         }
3293         element.style.display = visible ? "" : "none";
3294         element.previousElementSibling.style.display = visible ? "" : "none";
3295         if (parent) {
3296           element.nextElementSibling.style.display = visible ? "" : "none";
3297         }
3298       }
3299     };
3300 
3301   var editablePointsListener = function() {
3302       setVisible(dialog.getElement("x-start-input"), controller.isEditablePoints(), true);
3303       setVisible(dialog.getElement("x-end-input"), controller.isEditablePoints(), true);
3304       setVisible(dialog.getElement("distance-to-end-point-input"), controller.isEditablePoints());
3305       setVisible(dialog.getElement("arc-extent-input"), controller.isEditablePoints());
3306     };
3307   dialog.registerPropertyChangeListener(controller, "EDITABLE_POINTS", editablePointsListener);
3308   editablePointsListener(controller.isEditablePoints());
3309   
3310   return dialog;
3311 }
3312 
3313 /**
3314  * @param {UserPreferences} preferences
3315  * @param {RoomController} controller
3316  * @return {JSDialog}
3317  */
3318 JSViewFactory.prototype.createRoomView = function(preferences, controller) {
3319   var initFloorPanel = function(dialog) {
3320       // FLOOR_VISIBLE
3321       var floorVisibleDisplay = controller.isPropertyEditable("FLOOR_VISIBLE") ? "initial" : "none";
3322       dialog.floorVisibleCheckBox = dialog.getElement("floor-visible-checkbox");
3323       dialog.floorVisibleCheckBox.checked = controller.getFloorVisible();
3324       dialog.floorVisibleCheckBox.parentElement.style.display = floorVisibleDisplay;
3325       dialog.registerEventListener(dialog.floorVisibleCheckBox, "change", function(ev) {
3326           controller.setFloorVisible(dialog.floorVisibleCheckBox.checked);
3327         });
3328       dialog.registerPropertyChangeListener(controller, "FLOOR_VISIBLE", function(ev) {
3329           dialog.floorVisibleCheckBox.checked = controller.getFloorVisible(ev);
3330         });
3331   
3332       // FLOOR_PAINT
3333       var floorColorRadioButton = dialog.findElement("[name='floor-color-and-texture-choice'][value='COLORED']");
3334       dialog.floorColorButton = new ColorButton(preferences, 
3335           {
3336             colorChanged: function(selectedColor) {
3337               floorColorRadioButton.checked = true;
3338               controller.setFloorPaint(RoomController.RoomPaint.COLORED);
3339               controller.setFloorColor(selectedColor);
3340             }
3341           });
3342       dialog.attachChildComponent("floor-color-button", dialog.floorColorButton);
3343       dialog.floorColorButton.setColor(controller.getFloorColor());
3344       dialog.floorColorButton.setColorDialogTitle(preferences.getLocalizedString(
3345           "RoomPanel", "floorColorDialog.title"));
3346   
3347       var floorTextureRadioButton = dialog.findElement("[name='floor-color-and-texture-choice'][value='TEXTURED']");
3348       dialog.floorTextureComponent = controller.getFloorTextureController().getView();
3349       dialog.attachChildComponent("floor-texture-component", dialog.floorTextureComponent);
3350   
3351       dialog.registerEventListener([floorColorRadioButton, floorTextureRadioButton], "change", function(ev) {
3352           controller.setFloorPaint(RoomController.RoomPaint[this.value]);
3353         });
3354   
3355       var paintChangeListener = function() {
3356           floorColorRadioButton.checked = controller.getFloorPaint() == RoomController.RoomPaint.COLORED;
3357           floorTextureRadioButton.checked = controller.getFloorPaint() == RoomController.RoomPaint.TEXTURED;
3358         };
3359       dialog.registerPropertyChangeListener(controller, "FLOOR_PAINT", paintChangeListener);
3360   
3361       var floorPaintDisplay = controller.isPropertyEditable("FLOOR_PAINT") ? "initial" : "none";
3362       floorColorRadioButton.parentElement.parentElement.style.display = floorPaintDisplay;
3363       floorTextureRadioButton.parentElement.parentElement.style.display = floorPaintDisplay;
3364       dialog.getElement("floor-color-button").style.display = floorPaintDisplay;
3365       dialog.getElement("floor-texture-component").style.display = floorPaintDisplay;
3366   
3367       paintChangeListener();
3368       
3369       // FLOOR_SHININESS
3370       var shininessRadioButtons = dialog.findElements("[name='floor-shininess-choice']");
3371       dialog.registerEventListener(shininessRadioButtons, "change", function() {
3372         controller.setFloorShininess(parseFloat(this.value));
3373       });
3374   
3375       var shininessChangeListener = function() {
3376           for (var i = 0; i < shininessRadioButtons.length; i++) {
3377             shininessRadioButtons[i].checked = controller.getFloorShininess() == parseFloat(shininessRadioButtons[i].value);
3378           }
3379         };
3380       shininessChangeListener();
3381       dialog.registerPropertyChangeListener(controller, "FLOOR_SHININESS", shininessChangeListener);
3382   
3383       var floorShininessDisplay = controller.isPropertyEditable("FLOOR_SHININESS") ? "initial" : "none";
3384       shininessRadioButtons[0].parentElement.parentElement = floorShininessDisplay;
3385     };
3386 
3387   var initCeilingPanel = function(dialog) {
3388       // CEILING_VISIBLE
3389       var ceilingVisibleDisplay = controller.isPropertyEditable("CEILING_VISIBLE") ? "initial" : "none";
3390       dialog.ceilingVisibleCheckBox = dialog.getElement("ceiling-visible-checkbox");
3391       dialog.ceilingVisibleCheckBox.checked = controller.getCeilingVisible();
3392       dialog.ceilingVisibleCheckBox.parentElement.style.display = ceilingVisibleDisplay;
3393       dialog.registerEventListener(dialog.ceilingVisibleCheckBox, "change", function(ev) {
3394           controller.setCeilingVisible(dialog.ceilingVisibleCheckBox.checked);
3395         });
3396       dialog.registerPropertyChangeListener(controller, "CEILING_VISIBLE", function(ev) {
3397           dialog.ceilingVisibleCheckBox.checked = controller.getCeilingVisible();
3398         });
3399   
3400       // CEILING_PAINT
3401       var ceilingColorRadioButton = dialog.findElement("[name='ceiling-color-and-texture-choice'][value='COLORED']");
3402       dialog.ceilingColorButton = new ColorButton(preferences, 
3403           {
3404             colorChanged: function(selectedColor) {
3405               ceilingColorRadioButton.checked = true;
3406               controller.setCeilingPaint(RoomController.RoomPaint.COLORED);
3407               controller.setCeilingColor(selectedColor);
3408             }
3409           });
3410       dialog.attachChildComponent("ceiling-color-button", dialog.ceilingColorButton);
3411       dialog.ceilingColorButton.setColor(controller.getCeilingColor());
3412       dialog.ceilingColorButton.setColorDialogTitle(preferences.getLocalizedString(
3413           "RoomPanel", "ceilingColorDialog.title"));
3414   
3415       var ceilingTextureRadioButton = dialog.findElement("[name='ceiling-color-and-texture-choice'][value='TEXTURED']");
3416       dialog.ceilingTextureComponent = controller.getCeilingTextureController().getView();
3417       dialog.attachChildComponent("ceiling-texture-component", dialog.ceilingTextureComponent);
3418   
3419       dialog.registerEventListener([ceilingColorRadioButton, ceilingTextureRadioButton], "change", function(ev) {
3420           controller.setCeilingPaint(RoomController.RoomPaint[this.value]);
3421         });
3422   
3423       var paintChangeListener = function() {
3424           ceilingColorRadioButton.checked = controller.getCeilingPaint() == RoomController.RoomPaint.COLORED;
3425           ceilingTextureRadioButton.checked = controller.getCeilingPaint() == RoomController.RoomPaint.TEXTURED;
3426         };
3427       paintChangeListener();
3428       dialog.registerPropertyChangeListener(controller, "CEILING_PAINT", paintChangeListener);
3429   
3430       var ceilingPaintDisplay = controller.isPropertyEditable("CEILING_PAINT") ? "initial" : "none";
3431       ceilingColorRadioButton.parentElement.parentElement.style.display = ceilingPaintDisplay;
3432       ceilingTextureRadioButton.parentElement.parentElement.style.display = ceilingPaintDisplay;
3433       dialog.getElement("ceiling-color-button").style.display = ceilingPaintDisplay;
3434       dialog.getElement("ceiling-texture-component").style.display = ceilingPaintDisplay;
3435   
3436       // CEILING_SHININESS
3437       var shininessRadioButtons = dialog.findElements("[name='ceiling-shininess-choice']");
3438       dialog.registerEventListener(shininessRadioButtons, "change", function() {
3439           controller.setCeilingShininess(parseFloat(this.value));
3440         });
3441   
3442       var shininessChangeListener = function() {
3443         for (var i = 0; i < shininessRadioButtons.length; i++) {
3444           shininessRadioButtons[i].checked = controller.getCeilingShininess() == parseFloat(shininessRadioButtons[i].value);
3445         }
3446       }
3447       shininessChangeListener();
3448       dialog.registerPropertyChangeListener(controller, "CEILING_SHININESS", shininessChangeListener);
3449   
3450       var ceilingShininessDisplay = controller.isPropertyEditable("CEILING_SHININESS") ? "initial" : "none";
3451       shininessRadioButtons[0].parentElement.parentElement = ceilingShininessDisplay;
3452 
3453       // CEILING_FLAT
3454       dialog.ceilingFlatCheckBox = dialog.getElement("ceiling-flat-checkbox");
3455       dialog.ceilingFlatCheckBox.checked = controller.getCeilingFlat();
3456       dialog.ceilingFlatCheckBox.parentElement.style.display = 
3457           controller.isPropertyEditable("CEILING_FLAT") ? "initial" : "none";
3458       dialog.registerEventListener(dialog.ceilingFlatCheckBox, "change", function(ev) {
3459           controller.setCeilingFlat(dialog.ceilingFlatCheckBox.checked);
3460         });
3461       dialog.registerPropertyChangeListener(controller, "CEILING_FLAT", function(ev) {
3462           dialog.ceilingFlatCheckBox.checked = controller.getCeilingFlat(ev);
3463         });
3464     };
3465 
3466   var selectSplitSurroundingWallsAtFirstChange = function(dialog) {
3467       if (dialog.firstWallChange
3468           && dialog.splitSurroundingWallsCheckBox != null
3469           && !dialog.splitSurroundingWallsCheckBox.disabled) {
3470         dialog.splitSurroundingWallsCheckBox.checked = true;
3471         var ev = document.createEvent("Event");
3472         ev.initEvent("change", true, true);
3473         dialog.splitSurroundingWallsCheckBox.dispatchEvent(ev);
3474         dialog.firstWallChange = false;
3475       }
3476     };
3477 
3478    var initWallSidesPanel = function (dialog) {
3479       // SPLIT_SURROUNDING_WALLS
3480       var splitSurroundingWallsPropertyChanged = function() {
3481         dialog.splitSurroundingWallsCheckBox.disabled = !controller.isSplitSurroundingWallsNeeded();
3482         dialog.splitSurroundingWallsCheckBox.checked = controller.isSplitSurroundingWalls();
3483       }
3484   
3485       var splitSurroundingWallsDisplay = controller.isPropertyEditable("SPLIT_SURROUNDING_WALLS") ? "initial" : "none";
3486       dialog.splitSurroundingWallsCheckBox = dialog.getElement("split-surrounding-walls-checkbox");
3487       dialog.splitSurroundingWallsCheckBox.parentElement.style.display = splitSurroundingWallsDisplay;
3488       dialog.registerEventListener(dialog.splitSurroundingWallsCheckBox, "change", function(ev) {
3489           controller.setSplitSurroundingWalls(dialog.splitSurroundingWallsCheckBox.checked);
3490           dialog.firstWallChange = false;
3491         });
3492       splitSurroundingWallsPropertyChanged();
3493       dialog.registerPropertyChangeListener(controller, "SPLIT_SURROUNDING_WALLS", function(ev) {
3494           splitSurroundingWallsPropertyChanged();
3495         });
3496   
3497       // WALL_SIDES_PAINT
3498       var wallSidesColorRadioButton = dialog.findElement("[name='wall-sides-color-and-texture-choice'][value='COLORED']");
3499       dialog.wallSidesColorButton = new ColorButton(preferences, 
3500           {
3501             colorChanged: function(selectedColor) {
3502               wallSidesColorRadioButton.checked = true;
3503               controller.setWallSidesPaint(RoomController.RoomPaint.COLORED);
3504               controller.setWallSidesColor(selectedColor);
3505               selectSplitSurroundingWallsAtFirstChange(dialog);
3506             }
3507           });
3508       dialog.attachChildComponent("wall-sides-color-button", dialog.wallSidesColorButton);
3509       dialog.wallSidesColorButton.setColor(controller.getWallSidesColor());
3510       dialog.wallSidesColorButton.setColorDialogTitle(preferences.getLocalizedString(
3511           "RoomPanel", "wallSidesColorDialog.title"));
3512   
3513       var wallSidesTextureRadioButton = dialog.findElement("[name='wall-sides-color-and-texture-choice'][value='TEXTURED']");
3514       dialog.wallSidesTextureComponent = controller.getWallSidesTextureController().getView();
3515       dialog.registerPropertyChangeListener(controller.getWallSidesTextureController(), "TEXTURE", function(ev) {
3516            selectSplitSurroundingWallsAtFirstChange(dialog);
3517           });
3518       dialog.attachChildComponent("wall-sides-texture-component", dialog.wallSidesTextureComponent);
3519   
3520       dialog.registerEventListener([wallSidesColorRadioButton, wallSidesTextureRadioButton], "change", function(ev) {
3521           controller.setWallSidesPaint(RoomController.RoomPaint[this.value]);
3522         });
3523   
3524       var paintChangeListener = function() {
3525           wallSidesColorRadioButton.checked = controller.getWallSidesPaint() == RoomController.RoomPaint.COLORED;
3526           wallSidesTextureRadioButton.checked = controller.getWallSidesPaint() == RoomController.RoomPaint.TEXTURED;
3527         };
3528       paintChangeListener();
3529       dialog.registerPropertyChangeListener(controller, "WALL_SIDES_PAINT", paintChangeListener);
3530   
3531       var wallSidesPaintDisplay = controller.isPropertyEditable("WALL_SIDES_PAINT") ? "initial" : "none";
3532       wallSidesColorRadioButton.parentElement.parentElement.style.display = wallSidesPaintDisplay;
3533       wallSidesTextureRadioButton.parentElement.parentElement.style.display = wallSidesPaintDisplay;
3534       dialog.getElement("wall-sides-color-button").style.display = wallSidesPaintDisplay;
3535       dialog.getElement("wall-sides-texture-component").style.display = wallSidesPaintDisplay;
3536   
3537       // WALL_SIDES_SHININESS
3538       var shininessRadioButtons = dialog.findElements("[name='wall-sides-shininess-choice']");
3539       dialog.registerEventListener(shininessRadioButtons, "change", function(ev) {
3540           controller.setWallSidesShininess(parseFloat(this.value));
3541         });
3542   
3543       var shininessChangeListener = function() {
3544         for (var i = 0; i < shininessRadioButtons.length; i++) {
3545           shininessRadioButtons[i].checked = controller.getWallSidesShininess() == parseFloat(shininessRadioButtons[i].value);
3546         }
3547       }
3548       shininessChangeListener();
3549       dialog.registerPropertyChangeListener(controller, "WALL_SIDES_SHININESS", shininessChangeListener);
3550   
3551       var wallSidesShininessDisplay = controller.isPropertyEditable("WALL_SIDES_SHININESS") ? "initial" : "none";
3552       shininessRadioButtons[0].parentElement.parentElement.style.display = wallSidesShininessDisplay;
3553   
3554       if (wallSidesPaintDisplay == "none" && wallSidesShininessDisplay == "none") {
3555         dialog.getElement("wall-sides-panel").parentElement.style.display = "none";
3556       }
3557     };
3558 
3559   var initWallSidesBaseboardPanel = function(dialog) {
3560       if (!controller.isPropertyEditable("WALL_SIDES_BASEBOARD")) {
3561         dialog.getElement("wall-sides-baseboard-panel").parentElement.style.display = "none";
3562         return;
3563       }
3564   
3565       var baseboardComponentView = controller.getWallSidesBaseboardController().getView();
3566       dialog.attachChildComponent("wall-sides-baseboard-panel", baseboardComponentView);
3567       dialog.registerPropertyChangeListener(controller.getWallSidesBaseboardController(), "VISIBLE", function() {
3568           selectSplitSurroundingWallsAtFirstChange(dialog);
3569       });
3570     };
3571 
3572   var dialog = new JSDialog(preferences, 
3573       "@{RoomPanel.room.title}", 
3574       document.getElementById("room-dialog-template"), 
3575       {
3576         applier: function(dialog) {
3577           controller.modifyRooms();
3578         },
3579         disposer: function(dialog) {
3580           dialog.floorColorButton.dispose();
3581           dialog.floorTextureComponent.dispose();
3582           dialog.ceilingColorButton.dispose();
3583           dialog.ceilingTextureComponent.dispose();
3584           controller.getWallSidesBaseboardController().getView().dispose();
3585         },
3586       });
3587   
3588   var nameDisplay = controller.isPropertyEditable("NAME") ? "initial" : "none";
3589   dialog.nameInput = dialog.getElement("name-input");
3590   dialog.nameInput.value = controller.getName() != null ? controller.getName() : "";
3591   dialog.nameInput.parentElement.style.display = nameDisplay;
3592   dialog.registerEventListener(dialog.nameInput, "input", function(ev) {
3593       controller.setName(dialog.nameInput.value.trim());
3594     });
3595   dialog.registerPropertyChangeListener(controller, "NAME", function(ev) {
3596       dialog.nameInput.value = controller.getName() != null ? controller.getName() : "";
3597     });
3598 
3599   var areaVisiblePanelDisplay = controller.isPropertyEditable("AREA_VISIBLE") ? "initial" : "none";
3600   dialog.areaVisibleCheckBox = dialog.getElement("area-visible-checkbox");
3601   dialog.areaVisibleCheckBox.checked = controller.getAreaVisible();
3602   dialog.areaVisibleCheckBox.parentElement.style.display = areaVisiblePanelDisplay;
3603   dialog.registerEventListener(dialog.areaVisibleCheckBox, "change", function(ev) {
3604       controller.setAreaVisible(dialog.areaVisibleCheckBox.checked);
3605     });
3606   dialog.registerPropertyChangeListener(controller, "AREA_VISIBLE", function(ev) {
3607       dialog.areaVisibleCheckBox.checked = controller.getAreaVisible();
3608     });
3609 
3610   initFloorPanel(dialog);
3611   initCeilingPanel(dialog);
3612   initWallSidesPanel(dialog);
3613   initWallSidesBaseboardPanel(dialog);
3614 
3615   dialog.firstWallChange = true;
3616   return dialog;
3617 }
3618 
3619 /**
3620  * Creates a polyline editor dialog
3621  * @param {UserPreferences} preferences 
3622  * @param {PolylineController} controller 
3623  */
3624 JSViewFactory.prototype.createPolylineView = function(preferences, controller) {
3625   var initArrowsStyleComboBox = function(dialog) {
3626       var arrowsStyles = [];
3627       var arrowsStyleEnumValues = Object.keys(Polyline.ArrowStyle);
3628       for (var i = 0; i < arrowsStyleEnumValues.length; i++) {
3629         var arrowStyle = parseInt(arrowsStyleEnumValues[i]);
3630         if (!isNaN(arrowStyle)) {
3631           arrowsStyles.push(arrowStyle);
3632         }
3633       }
3634   
3635       /** @var {{ startStyle: number, endStyle: number }[]} */
3636       var arrowsStylesCombinations = [];
3637       for (var i = 0; i < arrowsStyles.length; i++) {
3638         for (var j = 0; j < arrowsStyles.length; j++) {
3639           arrowsStylesCombinations.push({ startStyle: arrowsStyles[i], endStyle: arrowsStyles[j] });
3640         }
3641       }
3642  
3643       var openStyleAtStart = new java.awt.geom.GeneralPath();
3644       openStyleAtStart.moveTo(15, 4);
3645       openStyleAtStart.lineTo(5, 8);
3646       openStyleAtStart.lineTo(15, 12);
3647       var deltaStyleAtStart = new java.awt.geom.GeneralPath();
3648       deltaStyleAtStart.moveTo(3, 8);
3649       deltaStyleAtStart.lineTo(15, 3);
3650       deltaStyleAtStart.lineTo(15, 13);
3651       deltaStyleAtStart.closePath();
3652       var iconWidth = 64;
3653       var openStyleAtEnd = new java.awt.geom.GeneralPath();
3654       openStyleAtEnd.moveTo(iconWidth - 14, 4);
3655       openStyleAtEnd.lineTo(iconWidth - 4, 8);
3656       openStyleAtEnd.lineTo(iconWidth - 14, 12);
3657       var deltaStyleAtEnd = new java.awt.geom.GeneralPath();
3658       deltaStyleAtEnd.moveTo(iconWidth - 2, 8);
3659       deltaStyleAtEnd.lineTo(iconWidth - 14, 3);
3660       deltaStyleAtEnd.lineTo(iconWidth - 14, 13);
3661       deltaStyleAtEnd.closePath();
3662       var comboBox = new JSComboBox(this.preferences, dialog.getElement("arrows-style-select"), 
3663           {
3664             nullable: controller.getCapStyle() == null,
3665             availableValues: arrowsStylesCombinations,
3666             renderCell: function(arrowStyle, itemElement) {
3667               itemElement.style.border = "none";
3668               itemElement.style.maxWidth = "6em";
3669               itemElement.style.margin = "auto";
3670               itemElement.style.textAlign = "center";
3671               
3672               var canvas = document.createElement("canvas");
3673               canvas.width = 200;
3674               canvas.height = 50;
3675               canvas.style.maxWidth = "100%";
3676               canvas.style.height = "1em";
3677               if (arrowStyle != null) {
3678                 var g2D = new Graphics2D(canvas);
3679                 g2D.scale(canvas.width / iconWidth, canvas.width / iconWidth);
3680                 g2D.setStroke(new java.awt.BasicStroke(2));
3681                 g2D.draw(new java.awt.geom.Line2D.Float(6, 8, iconWidth - 6, 8));
3682                 switch (arrowStyle.startStyle) {
3683                   case Polyline.ArrowStyle.DISC :
3684                     g2D.fill(new java.awt.geom.Ellipse2D.Float(4, 4, 9, 9));
3685                     break;
3686                   case Polyline.ArrowStyle.OPEN :
3687                     g2D.draw(openStyleAtStart);
3688                     break;
3689                   case Polyline.ArrowStyle.DELTA :
3690                     g2D.fill(deltaStyleAtStart);
3691                     break;
3692                 }
3693                 switch (arrowStyle.endStyle) {
3694                   case Polyline.ArrowStyle.DISC :
3695                     g2D.fill(new java.awt.geom.Ellipse2D.Float(iconWidth - 12, 4, 9, 9));
3696                     break;
3697                   case Polyline.ArrowStyle.OPEN :
3698                     g2D.draw(openStyleAtEnd);
3699                     break;
3700                   case Polyline.ArrowStyle.DELTA :
3701                     g2D.fill(deltaStyleAtEnd);
3702                     break;
3703                 }
3704                 
3705                 itemElement.appendChild(canvas);
3706               }
3707             },
3708             selectionChanged: function(newValue) {
3709               controller.setStartArrowStyle(newValue.startStyle);
3710               controller.setEndArrowStyle(newValue.endStyle);
3711             }
3712           });
3713   
3714       var arrowStyleChangeListener = function() {
3715           var startArrowStyle = controller.getStartArrowStyle();
3716           var endArrowStyle = controller.getEndArrowStyle();
3717           comboBox.setEnabled(controller.isArrowsStyleEditable());
3718           comboBox.setSelectedItem(
3719               { 
3720                 startStyle: startArrowStyle, 
3721                 endStyle: endArrowStyle 
3722               });
3723         };
3724       arrowStyleChangeListener();
3725       dialog.registerPropertyChangeListener(controller, "START_ARROW_STYLE", arrowStyleChangeListener);
3726       dialog.registerPropertyChangeListener(controller, "END_ARROW_STYLE", arrowStyleChangeListener);
3727     };
3728 
3729   var initJoinStyleComboBox = function(dialog) {
3730       var joinStyles = [];
3731       var joinStyleEnumValues = Object.keys(Polyline.JoinStyle);
3732       for (var i = 0; i < joinStyleEnumValues.length; i++) {
3733         var joinStyle = parseInt(joinStyleEnumValues[i]);
3734         if (!isNaN(joinStyle)) {
3735           joinStyles.push(joinStyle);
3736         }
3737       }
3738 
3739       var joinPath = new java.awt.geom.GeneralPath();
3740       joinPath.moveTo(10, 10);
3741       joinPath.lineTo(80, 10);
3742       joinPath.lineTo(50, 35);
3743       var curvedPath = new java.awt.geom.Arc2D.Float(10, 20, 80, 20, 0, 180, java.awt.geom.Arc2D.OPEN);
3744       var comboBox = new JSComboBox(this.preferences, dialog.getElement("join-style-select"), 
3745           {
3746             nullable: controller.getJoinStyle() == null,
3747             availableValues: joinStyles,
3748             renderCell: function(joinStyle, itemElement) {
3749               itemElement.style.border = "none";
3750               itemElement.style.textAlign = "center";
3751               
3752               var canvas = document.createElement("canvas");
3753               canvas.width = 100;
3754               canvas.height = 40;
3755               canvas.style.maxWidth = "100%";
3756               canvas.style.height = "1em";
3757               if (joinStyle != null) {
3758                 var g2D = new Graphics2D(canvas);
3759                 g2D.setStroke(ShapeTools.getStroke(8, Polyline.CapStyle.BUTT, joinStyle, null, 0));
3760                 if (joinStyle == Polyline.JoinStyle.CURVED) {
3761                   g2D.draw(curvedPath);
3762                 } else {
3763                   g2D.draw(joinPath);
3764                 }
3765               }
3766       
3767               itemElement.appendChild(canvas);
3768             },
3769             selectionChanged: function(newValue) {
3770               controller.setJoinStyle(newValue);
3771             }
3772           });
3773   
3774       var joinStyleChangeListener = function() {
3775           comboBox.setEnabled(controller.isJoinStyleEditable());
3776           comboBox.setSelectedItem(controller.getJoinStyle());
3777         };
3778       joinStyleChangeListener();
3779       dialog.registerPropertyChangeListener(controller, "JOIN_STYLE", joinStyleChangeListener);
3780     };
3781 
3782   var initDashStyleComboBox = function(dialog) {
3783       var dashStyles = [];
3784       var dashStyleEnumValues = Object.keys(Polyline.DashStyle);
3785       for (var i = 0; i < dashStyleEnumValues.length; i++) {
3786         var dashStyle = parseInt(dashStyleEnumValues[i]);
3787         if (!isNaN(dashStyle) 
3788             && (dashStyle != Polyline.DashStyle.CUSTOMIZED || controller.getDashStyle() == Polyline.DashStyle.CUSTOMIZED)) {
3789           dashStyles.push(dashStyle);
3790         }
3791       }
3792   
3793       var comboBox = new JSComboBox(this.preferences, dialog.getElement("dash-style-select"), 
3794           {
3795             nullable: controller.getDashStyle() == null,
3796             availableValues: dashStyles,
3797             renderCell: function(dashStyle, itemElement) {
3798               itemElement.style.border = "none";
3799               itemElement.style.textAlign = "center";
3800               itemElement.style.maxHeight = "2em";
3801               itemElement.style.minWidth = "4em";
3802       
3803               var canvas = document.createElement("canvas");
3804               canvas.width = 500;
3805               canvas.height = 100;
3806               canvas.style.maxWidth = "100%";
3807               canvas.style.height = "1em";
3808               var dashPattern = dashStyle != null && dashStyle != Polyline.DashStyle.CUSTOMIZED 
3809                   ? Polyline.DashStyle._$wrappers[dashStyle].getDashPattern() 
3810                   : controller.getDashPattern();
3811               if (dashPattern != null) {
3812                 var g2D = new Graphics2D(canvas);
3813                 var dashOffset = controller.getDashOffset() != null ? controller.getDashOffset() : 0;
3814                 g2D.setStroke(ShapeTools.getStroke(12, Polyline.CapStyle.BUTT, Polyline.JoinStyle.MITER,
3815                     dashPattern, dashOffset));
3816                 g2D.draw(new java.awt.geom.Line2D.Float(20, canvas.height / 2, canvas.width - 60, canvas.height / 2));
3817               }
3818       
3819               itemElement.appendChild(canvas);
3820             },
3821             selectionChanged: function(newValue) {
3822               controller.setDashStyle(newValue);
3823             }
3824           });
3825   
3826       var dashOffsetInput = new JSSpinner(preferences, dialog.getElement("dash-offset-input"), 
3827           {
3828             nullable: controller.getDashOffset() == null,
3829             value: controller.getDashOffset() == null ? null : controller.getDashOffset() * 100,
3830             minimum: 0,
3831             maximum: 100,
3832             stepSize: 5
3833           });
3834       dialog.registerEventListener(dashOffsetInput, "input", function(ev) {
3835           controller.setDashOffset(dashOffsetInput.getValue() != null
3836               ? dashOffsetInput.getValue() / 100
3837               : null);
3838         });
3839       dialog.registerPropertyChangeListener(controller, "DASH_OFFSET", function() {
3840           dashOffsetInput.setValue(controller.getDashOffset() == null ? null : controller.getDashOffset() * 100);
3841           comboBox.updateUI();
3842         });
3843   
3844       var dashStyleChangeListener = function() {
3845           dashOffsetInput.setEnabled(controller.getDashStyle() != Polyline.DashStyle.SOLID);
3846           comboBox.setSelectedItem(controller.getDashStyle());
3847         };
3848       dashStyleChangeListener();
3849       dialog.registerPropertyChangeListener(controller, "DASH_STYLE", dashStyleChangeListener);
3850     };
3851 
3852   var dialog = new JSDialog(preferences, 
3853       "@{PolylinePanel.polyline.title}", 
3854       document.getElementById("polyline-dialog-template"), 
3855       {
3856         size: "small",
3857         applier: function(dialog) {
3858           controller.modifyPolylines();
3859         },
3860         disposer: function(dialog) {
3861           dialog.colorButton.dispose();
3862         }
3863       });
3864 
3865   dialog.colorButton = new ColorButton(preferences, 
3866       {
3867         colorChanged: function(selectedColor) {
3868           controller.setColor(selectedColor);
3869         }
3870       });
3871   dialog.attachChildComponent("color-button", dialog.colorButton);
3872   dialog.colorButton.setColor(controller.getColor());
3873   dialog.colorButton.setColorDialogTitle(preferences.getLocalizedString(
3874       "PolylinePanel", "colorDialog.title"));
3875 
3876   dialog.thicknessLabelElement = dialog.getElement("thickness-label");
3877   dialog.thicknessLabelElement.textContent = dialog.getLocalizedLabelText(
3878       "PolylinePanel", "thicknessLabel.text", dialog.preferences.getLengthUnit().getName());
3879   
3880   dialog.thicknessInput = new JSSpinner(preferences, dialog.getElement("thickness-input"), 
3881       {
3882         nullable: controller.getThickness() == null,
3883         format: preferences.getLengthUnit().getFormat(),
3884         value: controller.getThickness(),
3885         minimum: preferences.getLengthUnit().getMinimumLength(),
3886         maximum: 50,
3887         stepSize: preferences.getLengthUnit().getStepSize()
3888       });
3889   dialog.registerPropertyChangeListener(controller, "THICKNESS", function(ev) {
3890       dialog.thicknessInput.setValue(controller.getThickness());
3891     });
3892   dialog.registerEventListener(dialog.thicknessInput, "input", function(ev) {
3893       controller.setThickness(dialog.thicknessInput.getValue());
3894     });
3895 
3896   initArrowsStyleComboBox(dialog);
3897   initJoinStyleComboBox(dialog);
3898   initDashStyleComboBox(dialog);
3899 
3900   dialog.visibleIn3DCheckBox = dialog.getElement("visible-in-3D-checkbox");        
3901   dialog.visibleIn3DCheckBox.checked = controller.isElevationEnabled() && controller.getElevation() != null;
3902   dialog.registerEventListener(dialog.visibleIn3DCheckBox, "change", function(ev) {
3903       if (dialog.visibleIn3DCheckBox.checked) {
3904         controller.setElevation(0);
3905       } else {
3906         controller.setElevation(null);
3907       }
3908     });
3909   return dialog;
3910 }
3911 
3912 JSViewFactory.prototype.createDimensionLineView = function(modification, preferences, controller) {
3913   var dialog = new JSDialog(preferences, 
3914     modification ? "@{DimensionLinePanel.dimensionLineModification.title}" : "@{DimensionLinePanel.dimensionLineCreation.title}",
3915     document.getElementById("dimension-line-dialog-template"), 
3916     {
3917       applier: function(dialog) {
3918         if (modification) {
3919           controller.modifyDimensionLines();
3920         } else {
3921           controller.createDimensionLine();
3922         }
3923       },
3924       disposer: function(dialog) {
3925         dialog.colorButton.dispose();
3926       },
3927       size: "small"
3928     });
3929 
3930   var maximumLength = preferences.getLengthUnit().getMaximumLength();
3931   var unitName = preferences.getLengthUnit().getName();
3932   
3933   // Spinner bound to X_START controller property
3934   dialog.xStartLabel = dialog.getElement("x-start-label");
3935   dialog.xStartLabel.textContent = dialog.getLocalizedLabelText("DimensionLinePanel", "xLabel.text", unitName);
3936   dialog.xStartInput = new JSSpinner(preferences, dialog.getElement("x-start-input"), 
3937       {
3938         nullable: controller.getXStart() == null,
3939         format: preferences.getLengthUnit().getFormat(),
3940         value: controller.getXStart(),
3941         minimum: -maximumLength,
3942         maximum: maximumLength,
3943         stepSize: preferences.getLengthUnit().getStepSize()
3944       });
3945   dialog.registerPropertyChangeListener(controller, "X_START", function(ev) {
3946       dialog.xStartInput.setValue(ev.getNewValue());
3947     });
3948   dialog.registerEventListener(dialog.xStartInput, "input", function(ev) {
3949       controller.setXStart(dialog.xStartInput.getValue());
3950     });
3951   
3952   // Spinner bound to Y_START controller property
3953   dialog.yStartLabel = dialog.getElement("y-start-label");
3954   dialog.yStartLabel.textContent = dialog.getLocalizedLabelText("DimensionLinePanel", "yLabel.text", unitName);
3955   dialog.yStartInput = new JSSpinner(preferences, dialog.getElement("y-start-input"), 
3956       {
3957         nullable: controller.getYStart() == null,
3958         format: preferences.getLengthUnit().getFormat(),
3959         value: controller.getYStart(),
3960         minimum: -maximumLength,
3961         maximum: maximumLength,
3962         stepSize: preferences.getLengthUnit().getStepSize()
3963       });
3964   dialog.registerPropertyChangeListener(controller, "Y_START", function(ev) {
3965       dialog.yStartInput.setValue(ev.getNewValue());
3966     });
3967   dialog.registerEventListener(dialog.yStartInput, "input", function(ev) {
3968       controller.setYStart(dialog.yStartInput.getValue());
3969     });
3970   
3971   // Spinner bound to ELEVATION_START controller property
3972   dialog.elevationStartLabel = dialog.getElement("elevation-start-label");
3973   dialog.elevationStartLabel.textContent = dialog.getLocalizedLabelText("DimensionLinePanel", "elevationLabel.text", unitName);
3974   dialog.elevationStartInput = new JSSpinner(preferences, dialog.getElement("elevation-start-input"), 
3975       {
3976         nullable: controller.getElevationStart() == null,
3977         format: preferences.getLengthUnit().getFormat(),
3978         value: controller.getElevationStart(),
3979         minimum: 0,
3980         maximum: maximumLength,
3981         stepSize: preferences.getLengthUnit().getStepSize()
3982       });
3983   dialog.registerPropertyChangeListener(controller, "ELEVATION_START", function(ev) {
3984       dialog.elevationStartInput.setValue(ev.getNewValue());
3985     });
3986   dialog.registerEventListener(dialog.elevationStartInput, "input", function(ev) {
3987       controller.setElevationStart(dialog.elevationStartInput.getValue());
3988     });
3989       
3990   // Spinner bound to X_END controller property
3991   dialog.xEndLabel = dialog.getElement("x-end-label");
3992   dialog.xEndLabel.textContent = dialog.getLocalizedLabelText("DimensionLinePanel", "xLabel.text", unitName);
3993   dialog.xEndInput = new JSSpinner(preferences, dialog.getElement("x-end-input"), 
3994       {
3995         nullable: controller.getXEnd() == null,
3996         format: preferences.getLengthUnit().getFormat(),
3997         value: controller.getXEnd(),
3998         minimum: -maximumLength,
3999         maximum: maximumLength,
4000         stepSize: preferences.getLengthUnit().getStepSize()
4001       });
4002   dialog.registerPropertyChangeListener(controller, "X_END", function(ev) {
4003       dialog.xEndInput.setValue(ev.getNewValue());
4004     });
4005   dialog.registerEventListener(dialog.xEndInput, "input", function(ev) {
4006       controller.setXEnd(dialog.xEndInput.getValue());
4007     });
4008 
4009   // Spinner bound to Y_END controller property
4010   dialog.yEndLabel = dialog.getElement("y-end-label");
4011   dialog.yEndLabel.textContent = dialog.getLocalizedLabelText("DimensionLinePanel", "yLabel.text", unitName);
4012   dialog.yEndInput = new JSSpinner(preferences, dialog.getElement("y-end-input"), 
4013       {
4014         nullable: controller.getYEnd() == null,
4015         format: preferences.getLengthUnit().getFormat(),
4016         value: controller.getYEnd(),
4017         minimum: -maximumLength,
4018         maximum: maximumLength,
4019         stepSize: preferences.getLengthUnit().getStepSize()
4020       });
4021   dialog.registerPropertyChangeListener(controller, "Y_END", function(ev) {
4022       dialog.yEndInput.setValue(ev.getNewValue());
4023     });
4024   dialog.registerEventListener(dialog.yEndInput, "input", function(ev) {
4025       controller.setYEnd(dialog.yEndInput.getValue());
4026     });
4027 
4028   // Spinner bound to DISTANCE_TO_END_POINT controller property
4029   dialog.distanceToEndPointLabel = dialog.getElement("distance-to-end-point-label");
4030   dialog.distanceToEndPointLabel.textContent = dialog.getLocalizedLabelText("DimensionLinePanel", "distanceToEndPointLabel.text", unitName);
4031   dialog.distanceToEndPointInput = new JSSpinner(preferences, dialog.getElement("distance-to-end-point-input"), 
4032       {
4033         nullable: controller.getDistanceToEndPoint() == null,
4034         format: preferences.getLengthUnit().getFormat(),
4035         value: controller.getDistanceToEndPoint(),
4036         minimum: preferences.getLengthUnit().getMinimumLength(),
4037         maximum: 2 * maximumLength * Math.sqrt(2),
4038         stepSize: preferences.getLengthUnit().getStepSize()
4039       });
4040   dialog.registerPropertyChangeListener(controller, "DISTANCE_TO_END_POINT", function(ev) {
4041       dialog.distanceToEndPointInput.setValue(ev.getNewValue());
4042     });
4043   dialog.registerEventListener(dialog.distanceToEndPointInput, "input", function(ev) {
4044       controller.setDistanceToEndPoint(dialog.distanceToEndPointInput.getValue());
4045     });
4046 
4047   // Spinner bound to OFFSET controller property
4048   dialog.offsetLabel = dialog.getElement("offset-label");
4049   dialog.offsetLabel.textContent = dialog.getLocalizedLabelText("DimensionLinePanel", "offsetLabel.text", unitName);
4050   dialog.offsetInput = new JSSpinner(preferences, dialog.getElement("offset-input"), 
4051       {
4052         nullable: controller.getOffset() == null,
4053         format: preferences.getLengthUnit().getFormat(),
4054         value: controller.getOffset(),
4055         minimum: -10000,
4056         maximum: 10000,
4057         stepSize: preferences.getLengthUnit().getStepSize()
4058       });
4059   dialog.registerPropertyChangeListener(controller, "OFFSET", function(ev) {
4060       dialog.offsetInput.setValue(ev.getNewValue());
4061     });
4062   dialog.registerEventListener(dialog.offsetInput, "input", function(ev) {
4063       controller.setOffset(dialog.offsetInput.getValue());
4064     });
4065   
4066   // Radio buttons bound to ORIENTATION controller property
4067   dialog.planDimensionLineRadioButton = dialog.findElement("[name='orientation-choice'][value='PLAN']");
4068   dialog.registerEventListener(dialog.planDimensionLineRadioButton, "change", function(ev) {
4069       if (dialog.planDimensionLineRadioButton.checked) {
4070         controller.setOrientation(DimensionLineController.DimensionLineOrientation.PLAN);
4071       }
4072     });
4073   dialog.elevationDimensionLineRadioButton = dialog.findElement("[name='orientation-choice'][value='ELEVATION']");
4074   dialog.registerEventListener(dialog.elevationDimensionLineRadioButton, "change", function(ev) {
4075       if (dialog.elevationDimensionLineRadioButton.checked) {
4076         controller.setOrientation(DimensionLineController.DimensionLineOrientation.ELEVATION);
4077       }
4078     });
4079   
4080   dialog.registerPropertyChangeListener(controller, "ORIENTATION", function(ev) {
4081       updateOrientationRadioButtons();
4082     });
4083 
4084   // Font size label and spinner bound to FONT_SIZE controller property
4085   dialog.lengthFontSizeLabel = dialog.getElement("length-font-size-label");
4086   dialog.lengthFontSizeLabel.textContent = dialog.getLocalizedLabelText(
4087       "DimensionLinePanel", "lengthFontSizeLabel.text", dialog.preferences.getLengthUnit().getName());
4088   dialog.lengthFontSizeInput = new JSSpinner(preferences, dialog.getElement("length-font-size-input"), 
4089       {
4090         format: preferences.getLengthUnit().getFormat(),
4091         value: controller.getLengthFontSize(),
4092         minimum: 5,
4093         maximum: 999,
4094         stepSize: preferences.getLengthUnit().getStepSize()
4095       });
4096   var lengthFontSizeChangeListener = function() {
4097       var fontSize = controller.getLengthFontSize();
4098       dialog.lengthFontSizeInput.setNullable(fontSize == null);
4099       dialog.lengthFontSizeInput.setValue(fontSize);
4100     };
4101   lengthFontSizeChangeListener();
4102   dialog.registerPropertyChangeListener(controller, "LENGTH_FONT_SIZE", lengthFontSizeChangeListener);
4103   dialog.registerEventListener(dialog.lengthFontSizeInput, "input", function(ev) {
4104       controller.setLengthFontSize(dialog.lengthFontSizeInput.getValue());
4105     });
4106     
4107   // Color button bound to controller COLOR controller property
4108   dialog.colorButton = new ColorButton(preferences,   
4109       {
4110         colorChanged: function(selectedColor) {
4111           controller.setColor(dialog.colorButton.getColor());
4112         }
4113       });
4114   dialog.attachChildComponent("color-button", dialog.colorButton);
4115   dialog.colorButton.setColor(controller.getColor());
4116   dialog.colorButton.setColorDialogTitle(preferences.getLocalizedString("DimensionLinePanel", "colorDialog.title"));
4117   dialog.registerPropertyChangeListener(controller, "COLOR", function() {
4118       dialog.colorButton.setColor(controller.getColor());
4119     });
4120 
4121   // Pitch components bound to PITCH controller property
4122   var updateOrientationRadioButtons = function() {
4123       if (controller.getOrientation() == DimensionLineController.DimensionLineOrientation.PLAN) {
4124         dialog.planDimensionLineRadioButton.checked = true;
4125       } else if (controller.getOrientation() == DimensionLineController.DimensionLineOrientation.ELEVATION) {
4126         dialog.elevationDimensionLineRadioButton.checked = true;
4127       }
4128       orientable = controller.isEditableDistance();
4129       dialog.planDimensionLineRadioButton.disabled = !orientable;
4130       dialog.elevationDimensionLineRadioButton.disabled = !orientable;
4131     
4132       if (controller.getPitch() != null
4133           && controller.getOrientation() != DimensionLineController.DimensionLineOrientation.ELEVATION) {
4134         if (controller.getPitch() === 0) {
4135           dialog.pitch0DegreeRadioButton.checked = true;
4136         } else if (Math.abs(controller.getPitch()) === Math.PI / 2) {
4137           dialog.pitch90DegreeRadioButton.checked = true;
4138         }
4139       }
4140 
4141       var planOrientation = controller.getOrientation() == DimensionLineController.DimensionLineOrientation.PLAN;
4142       var visibleIn3D = controller.isVisibleIn3D() === true;
4143       dialog.pitch0DegreeRadioButton.disabled = !(visibleIn3D && planOrientation);
4144       dialog.pitch90DegreeRadioButton.disabled = !(visibleIn3D && planOrientation);
4145       
4146       dialog.elevationStartInput.setEnabled(visibleIn3D
4147           || controller.getOrientation() == DimensionLineController.DimensionLineOrientation.ELEVATION);
4148       dialog.xEndInput.setEnabled(planOrientation);
4149       dialog.yEndInput.setEnabled(planOrientation);
4150     };
4151     
4152   dialog.visibleIn3DViewCheckBox = dialog.getElement("visible-in-3D-checkbox");
4153   dialog.visibleIn3DViewCheckBox.checked = controller.isVisibleIn3D();
4154   dialog.registerPropertyChangeListener(controller, "VISIBLE_IN_3D", function(ev) {
4155       dialog.visibleIn3DViewCheckBox.checked = controller.isVisibleIn3D();
4156     });
4157   dialog.registerEventListener(dialog.visibleIn3DViewCheckBox, "change", function(ev) {
4158       controller.setVisibleIn3D(dialog.visibleIn3DViewCheckBox.checked);
4159       updateOrientationRadioButtons();
4160     }); 
4161 
4162   dialog.pitch0DegreeRadioButton = dialog.findElement("[name='label-pitch-radio'][value='0']");
4163   dialog.pitch90DegreeRadioButton = dialog.findElement("[name='label-pitch-radio'][value='90']");
4164   var pitchRadioButtonsChangeListener = function() {
4165       if (dialog.pitch0DegreeRadioButton.checked) {
4166         controller.setPitch(0);
4167       } else if (dialog.pitch90DegreeRadioButton.checked) {
4168         controller.setPitch(-Math.PI / 2);
4169       }
4170     };
4171   dialog.registerEventListener([dialog.pitch0DegreeRadioButton, dialog.pitch90DegreeRadioButton], "change",
4172       pitchRadioButtonsChangeListener);
4173   dialog.registerPropertyChangeListener(controller, "PITCH", updateOrientationRadioButtons);
4174 
4175   updateOrientationRadioButtons();
4176   return dialog;
4177 }
4178 
4179 JSViewFactory.prototype.createLabelView = function(modification, preferences, controller) {
4180   var dialog = new JSDialog(preferences, 
4181       modification ? "@{LabelPanel.labelModification.title}": "@{LabelPanel.labelCreation.title}", 
4182       document.getElementById("label-dialog-template"), 
4183       {
4184         applier: function(dialog) {
4185           if (modification) {
4186             controller.modifyLabels();
4187           } else {
4188             controller.createLabel();
4189           }
4190         },
4191         disposer: function(dialog) {
4192           dialog.colorButton.dispose();
4193         },
4194         size: "small"
4195       });
4196 
4197   // Text field bound to NAME controller property
4198   dialog.textInput = dialog.getElement("text");
4199   dialog.textInput.value = controller.getText() != null ? controller.getText() : "";
4200   dialog.registerEventListener(dialog.textInput, "input", function(ev) {
4201       controller.setText(dialog.textInput.value);
4202     });
4203   dialog.registerPropertyChangeListener(controller, "TEXT", function(ev) {
4204       dialog.textInput.value = controller.getText() != null ? controller.getText() : "";
4205     });
4206   
4207   // Radio buttons bound to controller ALIGNMENT property
4208   dialog.alignmentRadioButtons = dialog.getHTMLElement().querySelectorAll("[name='label-alignment-radio']");
4209   dialog.registerEventListener(dialog.alignmentRadioButtons, "change", function(ev) {
4210       for (var i = 0; i < dialog.alignmentRadioButtons.length; i++) {
4211         if (dialog.alignmentRadioButtons[i].checked) {
4212           controller.setAlignment(TextStyle.Alignment[dialog.alignmentRadioButtons[i].value]);
4213         }
4214       }
4215     });
4216   var alignmentChangeListener = function() {
4217       var selectedAlignmentRadioButton = dialog.findElement("[name='label-alignment-radio'][value='" + TextStyle.Alignment[controller.getAlignment()] + "']");
4218       if (selectedAlignmentRadioButton != null) {
4219         selectedAlignmentRadioButton.checked = true;
4220       }
4221     };
4222   dialog.registerPropertyChangeListener(controller, "ALIGNMENT", alignmentChangeListener);
4223   alignmentChangeListener();
4224 
4225   // Font select bound to controller FONT_NAME property
4226   dialog.fontSelect = dialog.getElement("font-select");
4227   var DEFAULT_SYSTEM_FONT_NAME = "DEFAULT_SYSTEM_FONT_NAME";
4228   dialog.registerEventListener(dialog.fontSelect, "change", function(ev) {
4229       var selectedValue = dialog.fontSelect.querySelector("option:checked").value;
4230       controller.setFontName(selectedValue == DEFAULT_SYSTEM_FONT_NAME ? null : selectedValue);
4231     });
4232   var fontNameChangeListener = function() {
4233       if (controller.isFontNameSet()) {
4234         var selectedValue = controller.getFontName() == null 
4235             ? DEFAULT_SYSTEM_FONT_NAME 
4236             : controller.getFontName();
4237         var selectedOption = dialog.fontSelect.querySelector("[value='" + selectedValue + "']");
4238         if (selectedOption) {
4239           selectedOption.selected = true;
4240         }
4241       } else {
4242         dialog.fontSelect.selectedIndex = undefined;
4243       }
4244     };
4245   dialog.registerPropertyChangeListener(controller, "FONT_NAME", fontNameChangeListener);
4246   CoreTools.loadAvailableFontNames(function(fonts) {
4247       fonts = [DEFAULT_SYSTEM_FONT_NAME].concat(fonts);
4248       for (var i = 0; i < fonts.length; i++) {
4249         var font = fonts[i];
4250         var label = i == 0 ? dialog.getLocalizedLabelText("FontNameComboBox", "systemFontName") : font;
4251         dialog.fontSelect.appendChild(JSComponent.createOptionElement(font, label));
4252       }
4253       fontNameChangeListener();
4254     });
4255 
4256   // Font size label and spinner bound to FONT_SIZE controller property
4257   dialog.fontSizeLabel = dialog.getElement("font-size-label");
4258   dialog.fontSizeLabel.textContent = dialog.getLocalizedLabelText(
4259       "LabelPanel", "fontSizeLabel.text", dialog.preferences.getLengthUnit().getName());
4260   dialog.fontSizeInput = new JSSpinner(preferences, dialog.getElement("font-size-input"), 
4261       {
4262         format: preferences.getLengthUnit().getFormat(),
4263         value: controller.getFontSize(),
4264         minimum: 5,
4265         maximum: 999,
4266         stepSize: preferences.getLengthUnit().getStepSize()
4267       });
4268   var fontSizeChangeListener = function() {
4269       var fontSize = controller.getFontSize();
4270       dialog.fontSizeInput.setNullable(fontSize == null);
4271       dialog.fontSizeInput.setValue(fontSize);
4272     };
4273   fontSizeChangeListener();
4274   dialog.registerPropertyChangeListener(controller, "FONT_SIZE", fontSizeChangeListener);
4275   dialog.registerEventListener(dialog.fontSizeInput, "input", function(ev) {
4276       controller.setFontSize(dialog.fontSizeInput.getValue());
4277     });
4278     
4279   // Color button bound to controller COLOR controller property
4280   dialog.colorButton = new ColorButton(preferences,   
4281       {
4282         colorChanged: function(selectedColor) {
4283           controller.setColor(dialog.colorButton.getColor());
4284         }
4285       });
4286   dialog.attachChildComponent("color-button", dialog.colorButton);
4287   dialog.colorButton.setColor(controller.getColor());
4288   dialog.colorButton.setColorDialogTitle(preferences
4289       .getLocalizedString("LabelPanel", "colorDialog.title"));
4290   dialog.registerPropertyChangeListener(controller, "COLOR", function() {
4291       dialog.colorButton.setColor(controller.getColor());
4292     });
4293 
4294   // Pitch components bound to PITCH controller property
4295   var update3DViewComponents = function() {
4296       var visibleIn3D = controller.isPitchEnabled() === true;
4297       dialog.pitch0DegreeRadioButton.disabled = !visibleIn3D;
4298       dialog.pitch90DegreeRadioButton.disabled = !visibleIn3D;
4299       dialog.elevationInput.setEnabled(visibleIn3D);
4300       if (controller.getPitch() !== null) {
4301         if (controller.getPitch() === 0) {
4302           dialog.pitch0DegreeRadioButton.checked = true;
4303         } else if (controller.getPitch() === Math.PI / 2) {
4304           dialog.pitch90DegreeRadioButton.checked = true;
4305         }
4306       }
4307     };
4308   dialog.registerPropertyChangeListener(controller, "PITCH", update3DViewComponents);
4309     
4310   dialog.visibleIn3DCheckBox = dialog.getElement("visible-in-3D-checkbox");
4311   dialog.visibleIn3DCheckBox.checked = 
4312       controller.isPitchEnabled() !== null && controller.getPitch() !== null;
4313   dialog.registerEventListener(dialog.visibleIn3DCheckBox, "change", function(ev) {
4314       if (!dialog.visibleIn3DCheckBox.checked) {
4315         controller.setPitch(null);
4316       } else if (dialog.pitch90DegreeRadioButton.checked) {
4317         controller.setPitch(Math.PI / 2);
4318       } else {
4319         controller.setPitch(0);
4320       }
4321       update3DViewComponents();
4322     });
4323 
4324   dialog.pitch0DegreeRadioButton = dialog.findElement("[name='label-pitch-radio'][value='0']");
4325   dialog.pitch90DegreeRadioButton = dialog.findElement("[name='label-pitch-radio'][value='90']");
4326   var pitchRadioButtonsChangeListener = function() {
4327       if (dialog.pitch0DegreeRadioButton.checked) {
4328         controller.setPitch(0);
4329       } else if (dialog.pitch90DegreeRadioButton.checked) {
4330         controller.setPitch(Math.PI / 2);
4331       }
4332     };
4333   dialog.registerEventListener([dialog.pitch0DegreeRadioButton, dialog.pitch90DegreeRadioButton], "change",
4334       pitchRadioButtonsChangeListener);
4335   
4336   //  Elevation label and spinner bound to ELEVATION controller property
4337   dialog.elevationLabel = dialog.getElement("elevation-label");
4338   dialog.elevationLabel.textContent = dialog.getLocalizedLabelText(
4339       "LabelPanel", "elevationLabel.text", dialog.preferences.getLengthUnit().getName());
4340   dialog.elevationInput = new JSSpinner(preferences, dialog.getElement("elevation-input"), 
4341       {
4342         nullable: controller.getElevation() == null,
4343         format: preferences.getLengthUnit().getFormat(),
4344         value: controller.getElevation(),
4345         minimum: 0,
4346         maximum: preferences.getLengthUnit().getMaximumElevation(),
4347         stepSize: preferences.getLengthUnit().getStepSize()
4348       });
4349   var elevationChangeListener = function(ev) {
4350       dialog.elevationInput.setNullable(ev.getNewValue() === null);
4351       dialog.elevationInput.setValue(ev.getNewValue());
4352     };
4353   dialog.registerPropertyChangeListener(controller, "ELEVATION", elevationChangeListener);
4354   dialog.registerEventListener(dialog.elevationInput, "input", function(ev) {
4355       controller.setElevation(dialog.elevationInput.getValue());
4356     });
4357 
4358   update3DViewComponents();
4359   
4360   return dialog;
4361 }
4362 
4363 /**
4364  * @param {UserPreferences} preferences
4365  * @param {CompassController} controller
4366  * @return {JSCompassDialogView}
4367  */
4368 JSViewFactory.prototype.createCompassView = function(preferences, controller) {
4369   function CompassDialog() {
4370     this.controller = controller;
4371 
4372     JSDialog.call(this, preferences,
4373         "@{CompassPanel.compass.title}",
4374         document.getElementById("compass-dialog-template"),
4375         {
4376           size: "medium",
4377           applier: function(dialog) {
4378             dialog.controller.modifyCompass();
4379           }
4380         });
4381     
4382     this.initRosePanel();
4383     this.initGeographicLocationPanel();
4384   }
4385   CompassDialog.prototype = Object.create(JSDialog.prototype);
4386   CompassDialog.prototype.constructor = CompassDialog;
4387 
4388   /**
4389    * @private
4390    */
4391   CompassDialog.prototype.initRosePanel = function() {
4392       var preferences = this.preferences;
4393       var controller = this.controller;
4394   
4395       var maximumLength = preferences.getLengthUnit().getMaximumLength();
4396   
4397       var xLabel = this.getElement("x-label");
4398       var xInput = new JSSpinner(this.preferences, this.getElement("x-input"), 
4399           {
4400             format: preferences.getLengthUnit().getFormat(),
4401             minimum: -maximumLength,
4402             maximum: maximumLength,
4403             stepSize: preferences.getLengthUnit().getStepSize()
4404           });
4405       var yLabel = this.getElement("y-label");
4406       var yInput = new JSSpinner(this.preferences, this.getElement("y-input"), 
4407           {
4408             format: preferences.getLengthUnit().getFormat(),
4409             minimum: -maximumLength,
4410             maximum: maximumLength,
4411             stepSize: preferences.getLengthUnit().getStepSize()
4412           });
4413       var diameterLabel = this.getElement("diameter-label");
4414       var diameterInput = new JSSpinner(this.preferences, this.getElement("diameter-input"), 
4415           {
4416             format: preferences.getLengthUnit().getFormat(),
4417             minimum: preferences.getLengthUnit().getMinimumLength(),
4418             maximum: preferences.getLengthUnit().getMaximumLength() / 10,
4419             stepSize: preferences.getLengthUnit().getStepSize()
4420           });
4421   
4422       // Set values
4423       xInput.setValue(controller.getX());
4424       yInput.setValue(controller.getY());
4425       diameterInput.setValue(controller.getDiameter());
4426   
4427       // Set labels
4428       var unitName = this.preferences.getLengthUnit().getName();
4429       xLabel.textContent = this.getLocalizedLabelText("CompassPanel", "xLabel.text", unitName);
4430       yLabel.textContent = this.getLocalizedLabelText("CompassPanel", "yLabel.text", unitName);
4431       diameterLabel.textContent = this.getLocalizedLabelText("CompassPanel", "diameterLabel.text", unitName);
4432   
4433       // Add property listeners
4434       var controller = this.controller;
4435       this.registerPropertyChangeListener(this.controller, "X", function (ev) {
4436           xInput.setValue(controller.getX());
4437         });
4438       this.registerPropertyChangeListener(this.controller, "Y", function (ev) {
4439           yInput.setValue(controller.getY());
4440         });
4441       this.registerPropertyChangeListener(this.controller, "DIAMETER", function (ev) {
4442           diameterInput.setValue(controller.getDiameter());
4443         });
4444   
4445       // Add change listeners
4446       this.registerEventListener(xInput, "input", function (ev) {
4447           controller.setX(xInput.getValue());
4448         });
4449       this.registerEventListener(yInput, "input", function (ev) {
4450           controller.setY(yInput.getValue());
4451         });
4452       this.registerEventListener(diameterInput, "input", function (ev) {
4453           controller.setDiameter(diameterInput.getValue());
4454         });
4455   
4456       var visibleCheckBox = this.getElement("visible-checkbox");
4457       visibleCheckBox.checked = controller.isVisible();
4458       this.registerEventListener(visibleCheckBox, "change", function(ev) {
4459           controller.setVisible(visibleCheckBox.checked);
4460         });
4461       this.registerPropertyChangeListener(controller, "VISIBLE", function(ev) {
4462           visibleCheckBox.checked = controller.isVisible();
4463         });
4464     };
4465 
4466   /**
4467    * @private
4468    */
4469   CompassDialog.prototype.initGeographicLocationPanel = function() {
4470       var preferences = this.preferences;
4471       var controller = this.controller;
4472   
4473       var latitudeInput = new JSSpinner(this.preferences, this.getElement("latitude-input"), 
4474           {
4475             format: new DecimalFormat("N ##0.000;S ##0.000"),
4476             minimum: -90,
4477             maximum: 90,
4478             stepSize: 5
4479           });
4480       var longitudeInput = new JSSpinner(this.preferences, this.getElement("longitude-input"), 
4481           {
4482             format: new DecimalFormat("E ##0.000;W ##0.000"),
4483             minimum: -180,
4484             maximum: 180,
4485             stepSize: 5
4486           });
4487       var northDirectionInput = new JSSpinner(this.preferences, this.getElement("north-direction-input"), 
4488           {
4489             format: new IntegerFormat(),
4490             minimum: 0,
4491             maximum: 360,
4492             stepSize: 5
4493           });
4494       northDirectionInput.getHTMLElement().style.width = "3em";
4495       northDirectionInput.style.verticalAlign = "super";
4496   
4497       // Set values
4498       latitudeInput.setValue(controller.getLatitudeInDegrees());
4499       longitudeInput.setValue(controller.getLongitudeInDegrees());
4500       northDirectionInput.setValue(controller.getNorthDirectionInDegrees());
4501   
4502       // Add property listeners
4503       this.registerPropertyChangeListener(controller, "LATITUDE_IN_DEGREES", function(ev) {
4504           latitudeInput.setValue(controller.getLatitudeInDegrees());
4505         });
4506       this.registerPropertyChangeListener(controller, "LONGITUDE_IN_DEGREES", function(ev) {
4507           longitudeInput.setValue(controller.getLongitudeInDegrees());
4508         });
4509       this.registerPropertyChangeListener(controller, "NORTH_DIRECTION_IN_DEGREES", function(ev) {
4510           northDirectionInput.setValue(controller.getNorthDirectionInDegrees());
4511         });
4512   
4513       // Add change listeners
4514       this.registerEventListener(latitudeInput, "input", function(ev) {
4515           controller.setLatitudeInDegrees(latitudeInput.getValue());
4516         });
4517       this.registerEventListener(longitudeInput, "input", function(ev) {
4518           controller.setLongitudeInDegrees(longitudeInput.getValue());
4519         });
4520       this.registerEventListener(northDirectionInput, "input", function(ev) {
4521           controller.setNorthDirectionInDegrees(northDirectionInput.getValue());
4522           updatePreview();
4523         });
4524   
4525       var compassPreviewCanvas = this.getElement("compass-preview");
4526       compassPreviewCanvas.width = 140;
4527       compassPreviewCanvas.height = 140;
4528       compassPreviewCanvas.style.verticalAlign = "middle";
4529   
4530       compassPreviewCanvas.style.width = "35px";
4531   
4532       var compassPreviewCanvasContext = compassPreviewCanvas.getContext("2d");
4533       var canvasGraphics = new Graphics2D(compassPreviewCanvas);
4534   
4535       var updatePreview = function () {
4536           canvasGraphics.clear();
4537           var previousTransform = canvasGraphics.getTransform();
4538           canvasGraphics.translate(70, 70);
4539           canvasGraphics.scale(100, 100);
4540     
4541           canvasGraphics.setColor("#000000");
4542           canvasGraphics.fill(PlanComponent.COMPASS);
4543           canvasGraphics.setTransform(previousTransform);
4544     
4545           if (controller.getNorthDirectionInDegrees() == 0 || controller.getNorthDirectionInDegrees() == null) {
4546             compassPreviewCanvas.style.transform = "";
4547           } else {
4548             compassPreviewCanvas.style.transform = "rotate(" + controller.getNorthDirectionInDegrees() + "deg)";
4549           }
4550         };
4551       updatePreview();
4552     };
4553 
4554   return new CompassDialog();
4555 }
4556 
4557 JSViewFactory.prototype.createObserverCameraView = function(preferences, controller) {
4558   function ObserverCameraDialog() {
4559     this.controller = controller;
4560 
4561     JSDialog.call(this, preferences,
4562         "@{ObserverCameraPanel.observerCamera.title}",
4563         document.getElementById("observer-camera-dialog-template"),
4564         {
4565           applier: function(dialog) {
4566             dialog.controller.modifyObserverCamera();
4567           }
4568         });
4569 
4570     this.initLocationPanel();
4571     this.initAnglesPanel();
4572 
4573     var adjustObserverCameraElevationCheckBox = this.getElement("adjust-observer-camera-elevation-checkbox");
4574     adjustObserverCameraElevationCheckBox.checked = controller.isElevationAdjusted();
4575     var adjustObserverCameraElevationCheckBoxDisplay = controller.isObserverCameraElevationAdjustedEditable() ? "initial" : "none";
4576     adjustObserverCameraElevationCheckBox.parentElement.style.display = adjustObserverCameraElevationCheckBoxDisplay;
4577     this.registerEventListener(adjustObserverCameraElevationCheckBox, "change", function(ev) {
4578         controller.setElevationAdjusted(adjustObserverCameraElevationCheckBox.checked);
4579       });
4580     this.registerPropertyChangeListener(controller, "OBSERVER_CAMERA_ELEVATION_ADJUSTED", function(ev) {
4581         adjustObserverCameraElevationCheckBox.checked = controller.isElevationAdjusted();
4582       });
4583   }
4584   ObserverCameraDialog.prototype = Object.create(JSDialog.prototype);
4585   ObserverCameraDialog.prototype.constructor = ObserverCameraDialog;
4586 
4587   /**
4588    * @private
4589    */
4590   ObserverCameraDialog.prototype.initLocationPanel = function() {
4591     var maximumLength = 5E5;
4592     var xLabel = this.getElement("x-label");
4593     var xInput = new JSSpinner(this.preferences, this.getElement("x-input"), 
4594         {
4595           nullable: this.controller.getX() == null,
4596           format: this.preferences.getLengthUnit().getFormat(),
4597           minimum: -maximumLength,
4598           maximum: maximumLength,
4599           stepSize: this.preferences.getLengthUnit().getStepSize()
4600         });
4601     var yLabel = this.getElement("y-label");
4602     var yInput = new JSSpinner(this.preferences, this.getElement("y-input"), 
4603         {
4604           nullable: this.controller.getY() == null,
4605           format: this.preferences.getLengthUnit().getFormat(),
4606           minimum: -maximumLength,
4607           maximum: maximumLength,
4608           stepSize: this.preferences.getLengthUnit().getStepSize()
4609         });
4610     var elevationLabel = this.getElement("elevation-label");
4611     var elevationInput = new JSSpinner(this.preferences, this.getElement("elevation-input"), 
4612         {
4613           nullable: this.controller.getElevation() == null,
4614           format: this.preferences.getLengthUnit().getFormat(),
4615           minimum: this.controller.getMinimumElevation(),
4616           maximum: this.preferences.getLengthUnit().getMaximumElevation(),
4617           stepSize: this.preferences.getLengthUnit().getStepSize()
4618         });
4619 
4620     // Set values
4621     xInput.setValue(this.controller.getX());
4622     yInput.setValue(this.controller.getY());
4623     elevationInput.setValue(this.controller.getElevation());
4624 
4625     // Set labels
4626     var unitName = this.preferences.getLengthUnit().getName();
4627     xLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "xLabel.text", unitName);
4628     yLabel.textContent = this.getLocalizedLabelText("HomeFurniturePanel", "yLabel.text", unitName);
4629     elevationLabel.textContent = this.getLocalizedLabelText("ObserverCameraPanel", "elevationLabel.text", unitName);
4630 
4631     // Add property listeners
4632     var controller = this.controller;
4633     this.registerPropertyChangeListener(this.controller, "X", function (ev) {
4634         xInput.setValue(controller.getX());
4635       });
4636     this.registerPropertyChangeListener(this.controller, "Y", function (ev) {
4637         yInput.setValue(controller.getY());
4638       });
4639     this.registerPropertyChangeListener(this.controller, "ELEVATION", function (ev) {
4640         elevationInput.setValue(controller.getElevation());
4641       });
4642 
4643     // Add change listeners
4644     this.registerEventListener(xInput, "input", function (ev) {
4645         controller.setX(xInput.getValue());
4646       });
4647     this.registerEventListener(yInput, "input", function (ev) {
4648         controller.setY(yInput.getValue());
4649       });
4650     this.registerEventListener(elevationInput, "input", function (ev) {
4651         controller.setElevation(elevationInput.getValue());
4652       });
4653   };
4654 
4655   /**
4656    * @private
4657    */
4658   ObserverCameraDialog.prototype.initAnglesPanel = function() {
4659     var angleDecimalFormat = new DecimalFormat("0.#");
4660     var yawInput = new JSSpinner(this.preferences, this.getElement("yaw-input"), 
4661         {
4662           format: angleDecimalFormat,
4663           value: Math.toDegrees(this.controller.getYaw()),
4664           minimum: -10000,
4665           maximum: 10000,
4666           stepSize: 5
4667         });
4668     var pitchInput = new JSSpinner(this.preferences, this.getElement("pitch-input"), 
4669         {
4670           format: angleDecimalFormat,
4671           value: Math.toDegrees(this.controller.getPitch()),
4672           minimum: -90,
4673           maximum: 90,
4674           stepSize: 5
4675         });
4676     var fieldOfViewInput = new JSSpinner(this.preferences, this.getElement("field-of-view-input"), 
4677         {
4678           nullable: this.controller.getFieldOfView() == null,
4679           format: angleDecimalFormat,
4680           value: Math.toDegrees(this.controller.getFieldOfView()),
4681           minimum: 2,
4682           maximum: 120,
4683           stepSize: 1
4684         });
4685 
4686     // add property listeners
4687     var controller = this.controller;
4688     this.registerPropertyChangeListener(this.controller, "YAW", function (ev) {
4689       yawInput.setValue(Math.toDegrees(this.controller.getYaw()));
4690     });
4691     this.registerPropertyChangeListener(this.controller, "PITCH", function (ev) {
4692       pitchInput.setValue(Math.toDegrees(this.controller.getPitch()));
4693     });
4694     this.registerPropertyChangeListener(this.controller, "FIELD_OF_VIEW", function (ev) {
4695       fieldOfViewInput.setValue(Math.toDegrees(this.controller.getFieldOfView()));
4696     });
4697 
4698     // add change listeners
4699     this.registerEventListener(yawInput, "input", function (ev) {
4700         controller.setYaw(yawInput.getValue() != null ? Math.toRadians(yawInput.getValue()) : null);
4701       });
4702     this.registerEventListener(pitchInput, "input", function (ev) {
4703         controller.setPitch(pitchInput.getValue() != null ? Math.toRadians(pitchInput.getValue()) : null);
4704       });
4705     this.registerEventListener(fieldOfViewInput, "input", function (ev) {
4706         controller.setFieldOfView(fieldOfViewInput.getValue() != null ? Math.toRadians(fieldOfViewInput.getValue()) : null);
4707       });
4708   };
4709 
4710   return new ObserverCameraDialog();
4711 }
4712 
4713 JSViewFactory.prototype.createHome3DAttributesView = function(preferences, controller) {
4714   function Home3DAttributesDialog() {
4715     this.controller = controller;
4716 
4717     JSDialog.call(this, preferences,
4718         "@{Home3DAttributesPanel.home3DAttributes.title}",
4719         document.getElementById("home-3Dattributes-dialog-template"),
4720         {
4721           size: "small",
4722           applier: function(dialog) {
4723             dialog.controller.modify3DAttributes();
4724           },
4725           disposer: function(dialog) {
4726             dialog.groundPanel.colorButton.dispose();
4727             dialog.groundPanel.textureComponent.dispose();
4728             dialog.skyPanel.colorButton.dispose();
4729             dialog.skyPanel.textureComponent.dispose();
4730           }
4731         });
4732     
4733 
4734     this.initGroundPanel();
4735     this.initSkyPanel();
4736     this.initRenderingPanel();
4737   }
4738   Home3DAttributesDialog.prototype = Object.create(JSDialog.prototype);
4739   Home3DAttributesDialog.prototype.constructor = Home3DAttributesDialog;
4740 
4741   /**
4742    * @private
4743    */
4744   Home3DAttributesDialog.prototype.initGroundPanel = function() {
4745     var controller = this.controller;
4746     var dialog = this;
4747 
4748     var groundColorRadioButton = dialog.findElement("[name='ground-color-and-texture-choice'][value='COLORED']");
4749     var groundColorButton = new ColorButton(preferences,  
4750         {
4751           colorChanged: function(selectedColor) {
4752             groundColorRadioButton.checked = true;
4753             controller.setGroundPaint(Home3DAttributesController.EnvironmentPaint.COLORED);
4754             controller.setGroundColor(selectedColor);
4755           }
4756         });
4757     dialog.attachChildComponent("ground-color-button", groundColorButton);
4758     groundColorButton.setColor(controller.getGroundColor());
4759     groundColorButton.setColorDialogTitle(preferences.getLocalizedString(
4760         "Home3DAttributesPanel", "groundColorDialog.title"));
4761 
4762     var groundTextureRadioButton = dialog.findElement("[name='ground-color-and-texture-choice'][value='TEXTURED']");
4763     var textureComponent = controller.getGroundTextureController().getView();
4764     dialog.attachChildComponent("ground-texture-component", textureComponent);
4765 
4766     var radioButtons = [groundColorRadioButton, groundTextureRadioButton];
4767     dialog.registerEventListener(radioButtons, "change", function(ev) {
4768         if (ev.target.checked) {
4769           controller.setGroundPaint(Home3DAttributesController.EnvironmentPaint[ev.target.value]);
4770         }
4771       });
4772 
4773     var paintChangeListener = function() {
4774         groundColorRadioButton.checked = controller.getGroundPaint() == Home3DAttributesController.EnvironmentPaint.COLORED;
4775         groundTextureRadioButton.checked = controller.getGroundPaint() == Home3DAttributesController.EnvironmentPaint.TEXTURED;
4776       };
4777     paintChangeListener();
4778     this.registerPropertyChangeListener(controller, "GROUND_PAINT", paintChangeListener);
4779     this.registerPropertyChangeListener(controller, "GROUND_COLOR", function(ev) {
4780         groundColorButton.setColor(controller.getGroundColor());
4781       });
4782 
4783     var backgroundImageVisibleOnGround3DCheckBox = this.getElement("background-image-visible-on-ground-3D-checkbox");
4784     backgroundImageVisibleOnGround3DCheckBox.checked = controller.isBackgroundImageVisibleOnGround3D();
4785     this.registerEventListener(backgroundImageVisibleOnGround3DCheckBox, "change", function(ev) {
4786         controller.setBackgroundImageVisibleOnGround3D(backgroundImageVisibleOnGround3DCheckBox.checked);
4787       });
4788     this.registerPropertyChangeListener(controller, "BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D", function(ev) {
4789         backgroundImageVisibleOnGround3DCheckBox.checked = controller.isBackgroundImageVisibleOnGround3D();
4790       });
4791 
4792     this.groundPanel = {
4793         colorButton: groundColorButton,
4794         textureComponent: textureComponent,
4795       };
4796   };
4797 
4798   /**
4799    * @private
4800    */
4801   Home3DAttributesDialog.prototype.initSkyPanel = function() {
4802     var controller = this.controller;
4803     var dialog = this;
4804 
4805     var skyColorRadioButton = dialog.findElement("[name='sky-color-and-texture-choice'][value='COLORED']");
4806     var skyColorButton = new ColorButton(preferences,  
4807         {
4808           colorChanged: function(selectedColor) {
4809             skyColorRadioButton.checked = true;
4810             controller.setSkyPaint(Home3DAttributesController.EnvironmentPaint.COLORED);
4811             controller.setSkyColor(selectedColor);
4812           }
4813         });
4814     dialog.attachChildComponent("sky-color-button", skyColorButton);
4815     skyColorButton.setColor(controller.getSkyColor());
4816     skyColorButton.setColorDialogTitle(preferences.getLocalizedString(
4817         "Home3DAttributesPanel", "skyColorDialog.title"));
4818 
4819     var skyTextureRadioButton = dialog.findElement("[name='sky-color-and-texture-choice'][value='TEXTURED']");
4820     var textureComponent = controller.getSkyTextureController().getView();
4821     dialog.attachChildComponent("sky-texture-component", textureComponent);
4822 
4823     var radioButtons = [skyColorRadioButton, skyTextureRadioButton];
4824     dialog.registerEventListener(radioButtons, "change", function(ev) {
4825         if (ev.target.checked) {
4826           controller.setSkyPaint(Home3DAttributesController.EnvironmentPaint[ev.target.value]);
4827         }
4828       });
4829 
4830     var paintChangeListener = function() {
4831         skyColorRadioButton.checked = controller.getSkyPaint() == Home3DAttributesController.EnvironmentPaint.COLORED;
4832         skyTextureRadioButton.checked = controller.getSkyPaint() == Home3DAttributesController.EnvironmentPaint.TEXTURED;
4833       };
4834     paintChangeListener();
4835     this.registerPropertyChangeListener(controller, "SKY_PAINT", paintChangeListener);
4836     this.registerPropertyChangeListener(controller, "SKY_COLOR", function() {
4837         skyColorButton.setColor(controller.getSkyColor());
4838       });
4839 
4840     this.skyPanel = {
4841         colorButton: skyColorButton,
4842         textureComponent: textureComponent,
4843       };
4844   };
4845 
4846   /**
4847    * @private
4848    */
4849   Home3DAttributesDialog.prototype.initRenderingPanel = function() {
4850     var controller = this.controller;
4851 
4852     var brightnessSlider = this.getElement("brightness-slider");
4853     var brightnessList = this.findElement("#home-3Dattributes-brightness-list");
4854 
4855     var wallsTransparencySlider = this.getElement("walls-transparency-slider");
4856     var wallsTransparencyList = this.findElement("#home-3Dattributes-walls-transparency-list");
4857 
4858     for (var i = 0; i <= 255; i+= 17) {
4859       var option = document.createElement("option");
4860       option.value = i;
4861       brightnessList.appendChild(option);
4862       wallsTransparencyList.appendChild(option.cloneNode());
4863     }
4864 
4865     brightnessSlider.value = controller.getLightColor() & 0xFF;
4866     wallsTransparencySlider.value = controller.getWallsAlpha() * 255;
4867 
4868     this.registerEventListener(brightnessSlider, "input", function(ev) {
4869         var brightness = ev.target.value;
4870         controller.setLightColor((brightness << 16) | (brightness << 8) | brightness);
4871       });
4872     this.registerEventListener(wallsTransparencySlider, "input", function(ev) {
4873         controller.setWallsAlpha(this.value / 255);
4874       });
4875 
4876     this.registerPropertyChangeListener(controller, "LIGHT_COLOR", function(ev) {
4877         brightnessSlider.value = controller.getLightColor() & 0xFF;
4878       });
4879     this.registerPropertyChangeListener(controller, "WALLS_ALPHA", function(ev) {
4880         wallsTransparencySlider.value = controller.getWallsAlpha() * 255;
4881       });
4882   };
4883 
4884   return new Home3DAttributesDialog();
4885 }
4886 
4887 /**
4888  * Creates a texture selection component
4889  * @param {UserPreferences} preferences current user's preferences 
4890  * @param {TextureChoiceController} textureChoiceController texture choice controller
4891  * @return {JSComponent} 
4892  */
4893 JSViewFactory.prototype.createTextureChoiceView = function(preferences, textureChoiceController) {
4894   return new TextureChoiceComponent(preferences, textureChoiceController);
4895 }
4896 
4897 JSViewFactory.prototype.createBaseboardChoiceView = function(preferences, controller) {
4898   function BaseboardChoiceComponent() {
4899     JSComponent.call(this, preferences,
4900           "  <div class='whole-line'>"
4901         + "    <label>"
4902         + "      <input name='baseboard-visible-checkbox' type='checkbox'/>"
4903         + "      @{BaseboardChoiceComponent.visibleCheckBox.text}"
4904         + "    </label>"
4905         + "  </div>"
4906         + ""
4907         + "  <div class='whole-line'>"
4908         + "    <label>"
4909         + "      <input type='radio' name='baseboard-color-and-texture-choice' value='sameColorAsWall'/>"
4910         + "      @{BaseboardChoiceComponent.sameColorAsWallRadioButton.text}"
4911         + "    </label>"
4912         + "  </div>"
4913         + "  <div>"
4914         + "    <label>"
4915         + "      <input type='radio' name='baseboard-color-and-texture-choice' value='COLORED'>"
4916         + "        @{BaseboardChoiceComponent.colorRadioButton.text}"
4917         + "    </label>"
4918         + "  </div>"
4919         + "  <div data-name='baseboard-color-button'></div>"
4920         + "  <div>"
4921         + "    <label>"
4922         + "      <input type='radio' name='baseboard-color-and-texture-choice' value='TEXTURED'>"
4923         + "        @{BaseboardChoiceComponent.textureRadioButton.text}"
4924         + "    </label>"
4925         + "  </div>"
4926         + "  <div data-name='baseboard-texture-component'></div>"
4927         + "  <div class='whole-line'>"
4928         + "    <hr/>"
4929         + "  </div>"
4930         + "  <div data-name='height-label' class='label-cell'></div>"
4931         + "  <div><span data-name='height-input'></span></div>"
4932         + "  <div data-name='thickness-label' class='label-cell'></div>"
4933         + "  <div><span data-name='thickness-input'></span></div>");
4934 
4935     this.initComponents(controller);
4936   }
4937   BaseboardChoiceComponent.prototype = Object.create(JSComponent.prototype);
4938   BaseboardChoiceComponent.prototype.constructor = BaseboardChoiceComponent;
4939   
4940   BaseboardChoiceComponent.prototype.dispose = function () {
4941     this.colorButton.dispose();
4942     this.textureComponent.dispose();
4943     JSComponent.prototype.dispose.call(this);
4944   }
4945   
4946   /**
4947    * @private
4948    */
4949   BaseboardChoiceComponent.prototype.initComponents = function (controller) {
4950     var component = this;
4951     this.getHTMLElement().dataset["name"] = "baseboard-panel";
4952     this.getHTMLElement().classList.add("label-input-grid");
4953 
4954     // VISIBLE
4955     this.visibleCheckBox = this.getElement("baseboard-visible-checkbox");
4956     this.visibleCheckBox.checked = controller.getVisible();
4957     this.registerEventListener(this.visibleCheckBox, "change", function(ev) {
4958         controller.setVisible(component.visibleCheckBox.checked);
4959       });
4960 
4961     var visibleChanged = function() {
4962         var visible = controller.getVisible();
4963         component.visibleCheckBox.checked = visible;
4964         var componentsEnabled = visible !== false;
4965         for (var i = 0; i < component.colorAndTextureRadioButtons.length; i++) {
4966           component.colorAndTextureRadioButtons[i].disabled = !componentsEnabled;
4967         }
4968         component.colorButton.setEnabled(componentsEnabled);
4969         component.textureComponent.setEnabled(componentsEnabled);
4970         component.heightInput.setEnabled(componentsEnabled);
4971         component.thicknessInput.setEnabled(componentsEnabled);
4972       };
4973     this.registerPropertyChangeListener(controller, "VISIBLE", function(ev) {
4974         visibleChanged();
4975       });
4976 
4977     // PAINT
4978     var sameColorAsWallRadioButton = this.findElement("[name='baseboard-color-and-texture-choice'][value='sameColorAsWall']");
4979 
4980     var colorRadioButton = this.findElement("[name='baseboard-color-and-texture-choice'][value='COLORED']");
4981     this.colorButton = new ColorButton(preferences, 
4982         {
4983           colorChanged: function(selectedColor) {
4984             colorRadioButton.checked = true;
4985             controller.setPaint(BaseboardChoiceController.BaseboardPaint.COLORED);
4986             controller.setColor(selectedColor);
4987           }
4988         });
4989     this.attachChildComponent("baseboard-color-button", this.colorButton);
4990     this.colorButton.setColor(controller.getColor());
4991     this.colorButton.setColorDialogTitle(preferences.getLocalizedString(
4992         "BaseboardChoiceComponent", "colorDialog.title"));
4993 
4994     var textureRadioButton = this.findElement("[name='baseboard-color-and-texture-choice'][value='TEXTURED']");
4995     this.textureComponent = controller.getTextureController().getView();
4996     this.attachChildComponent("baseboard-texture-component", this.textureComponent);
4997 
4998     this.colorAndTextureRadioButtons = [sameColorAsWallRadioButton, colorRadioButton, textureRadioButton];
4999     this.registerEventListener(this.colorAndTextureRadioButtons, "change", function(ev) {
5000         if (ev.target.checked) {
5001           var selectedPaint = ev.target.value == "sameColorAsWall"
5002               ? BaseboardChoiceController.BaseboardPaint.DEFAULT
5003               : BaseboardChoiceController.BaseboardPaint[ev.target.value];
5004           controller.setPaint(selectedPaint);
5005         }
5006       });
5007 
5008     var paintChangeListener = function() {
5009         sameColorAsWallRadioButton.checked = controller.getPaint() == BaseboardChoiceController.BaseboardPaint.DEFAULT;
5010         colorRadioButton.checked = controller.getPaint() == BaseboardChoiceController.BaseboardPaint.COLORED;
5011         textureRadioButton.checked = controller.getPaint() == BaseboardChoiceController.BaseboardPaint.TEXTURED;
5012       };
5013     paintChangeListener();
5014     this.registerPropertyChangeListener(controller, "PAINT", paintChangeListener);
5015 
5016     // Height & thickness
5017     var unitName = preferences.getLengthUnit().getName();
5018     this.getElement("height-label").textContent = this.getLocalizedLabelText("BaseboardChoiceComponent", "heightLabel.text", unitName);
5019     this.getElement("thickness-label").textContent = this.getLocalizedLabelText("BaseboardChoiceComponent", "thicknessLabel.text", unitName);
5020 
5021     var minimumLength = preferences.getLengthUnit().getMinimumLength();
5022     this.heightInput = new JSSpinner(preferences, this.getElement("height-input"), 
5023         {
5024           nullable: controller.getHeight() == null,
5025           format: preferences.getLengthUnit().getFormat(),
5026           value: controller.getHeight() != null && controller.getMaxHeight() != null
5027               ? Math.min(controller.getHeight(), controller.getMaxHeight())
5028               : controller.getHeight(),
5029           minimum: minimumLength,
5030           maximum: controller.getMaxHeight() == null
5031               ? preferences.getLengthUnit().getMaximumLength() / 10
5032               : controller.getMaxHeight(),
5033           stepSize: preferences.getLengthUnit().getStepSize()
5034         });
5035     this.thicknessInput = new JSSpinner(preferences, this.getElement("thickness-input"), 
5036         {
5037           nullable: controller.getThickness() == null,
5038           format: preferences.getLengthUnit().getFormat(),
5039           value: controller.getThickness(),
5040           minimum: minimumLength,
5041           maximum: 2,
5042           stepSize: preferences.getLengthUnit().getStepSize()
5043         });
5044 
5045     this.registerPropertyChangeListener(controller, "HEIGHT", function(ev) {
5046         component.heightInput.setValue(ev.getNewValue());
5047       });
5048     this.registerPropertyChangeListener(controller, "MAX_HEIGHT", function(ev) {
5049         if (ev.getOldValue() == null
5050             || controller.getMaxHeight() != null
5051             && component.heightInput.max < controller.getMaxHeight()) {
5052           // Change max only if larger value to avoid taking into account intermediate max values
5053           // that may be fired by auto commit spinners while entering a value
5054           component.heightInput.max = controller.getMaxHeight();
5055         }
5056       });
5057     this.registerPropertyChangeListener(controller, "THICKNESS", function(ev) {
5058         component.thicknessInput.setValue(ev.getNewValue());
5059       });
5060 
5061     this.registerEventListener(this.heightInput, "input", function(ev) {
5062         controller.setHeight(component.heightInput.getValue());
5063       });
5064     this.registerEventListener(this.thicknessInput, "input", function(ev) {
5065         controller.setThickness(component.thicknessInput.getValue());
5066       });
5067 
5068     visibleChanged();
5069   }
5070 
5071   return new BaseboardChoiceComponent();
5072 }
5073 
5074 JSViewFactory.prototype.createModelMaterialsView = function(preferences, controller) {
5075   return new ModelMaterialsComponent(preferences, controller);
5076 }
5077 
5078 JSViewFactory.prototype.createPageSetupView = function(preferences, pageSetupController) {
5079   return this.dummyDialogView;
5080 }
5081 
5082 JSViewFactory.prototype.createPrintPreviewView = function(home, preferences, homeController, printPreviewController) {
5083   return this.dummyDialogView;
5084 }
5085 
5086 JSViewFactory.prototype.createPhotoView = function(home, preferences, photoController) {
5087   return this.dummyDialogView;
5088 }
5089 
5090 JSViewFactory.prototype.createPhotosView = function(home, preferences, photosController) {
5091   return this.dummyDialogView;
5092 }
5093 
5094 JSViewFactory.prototype.createVideoView = function(home, preferences, videoController) {
5095   return this.dummyDialogView;
5096 }
5097