1 /*
  2  * PlanComponent.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 /**
 22  * Creates a new plan that displays <code>home</code>.
 23  * @param {string} containerOrCanvasId the ID of a HTML DIV or CANVAS
 24  * @param {Home} home the home to display
 25  * @param {UserPreferences} preferences user preferences to retrieve used unit, grid visibility...
 26  * @param {Object} [object3dFactory] a factory able to create 3D objects from <code>home</code> furniture.
 27  * The {@link Object3DFactory#createObject3D(Home, Selectable, boolean) createObject3D} of
 28  * this factory is expected to return an instance of {@link Object3DBranch} in current implementation.
 29  * @param {PlanController} controller the optional controller used to manage home items modification
 30  * @constructor
 31  * @author Emmanuel Puybaret
 32  * @author Renaud Pawlak
 33  */
 34 function PlanComponent(containerOrCanvasId, home, preferences, object3dFactory, controller) {
 35   this.home = home;
 36   this.preferences = preferences;
 37   if (controller == null) {
 38     controller = object3dFactory;
 39     object3dFactory = new Object3DBranchFactory();
 40   }  
 41   this.object3dFactory = object3dFactory;
 42   
 43   var plan = this;
 44   this.pointerType = View.PointerType.MOUSE; 
 45   this.canvasNeededRepaint = false;
 46   this.container = document.getElementById(containerOrCanvasId);
 47   var computedStyle = window.getComputedStyle(this.container);
 48   this.font = [computedStyle.fontStyle, computedStyle.fontSize, computedStyle.fontFamily].join(' ');
 49   if(computedStyle.position != "absolute") {
 50     this.container.style.position = "relative";
 51   }
 52   if (this.container instanceof HTMLCanvasElement) {
 53     this.canvas = this.view = this.container; // No scrollPane
 54     this.canvas.width = this.canvas.clientWidth; 
 55     this.canvas.height = this.canvas.clientHeight;
 56   } else {
 57     this.canvas = document.createElement("canvas");
 58     this.canvas.setAttribute("id", containerOrCanvasId + ".canvas");
 59     this.canvas.style.width = "100%"; // computedStyle.width;
 60     this.canvas.style.height = "100%"; // computedStyle.height;
 61     if (PlanComponent.initialBackgroundColor === undefined) {
 62       PlanComponent.initialBackgroundColor = computedStyle.backgroundColor;
 63       PlanComponent.initialForegroundColor = computedStyle.color;
 64     }
 65     this.canvas.style.backgroundColor = PlanComponent.initialBackgroundColor;  // /!\ computedStyle.backgroundColor and color may change when reseting home
 66     this.canvas.style.color = PlanComponent.initialForegroundColor;
 67     this.canvas.style.font = computedStyle.font;
 68     this.scrollPane = document.createElement("div");
 69     this.scrollPane.setAttribute("id", containerOrCanvasId + ".scrollPane");
 70     this.scrollPane.style.width = "100%"; // computedStyle.width;
 71     this.scrollPane.style.height = "100%"; // computedStyle.height;
 72     if (this.container.style.overflow) {
 73       this.scrollPane.style.overflow = this.container.style.overflow;
 74     } else {
 75       this.scrollPane.style.overflow = "scroll";
 76     }
 77     this.view = document.createElement("div");
 78     this.view.setAttribute("id", containerOrCanvasId + ".view");
 79     this.container.appendChild(this.scrollPane);
 80     this.container.appendChild(this.canvas);
 81     this.scrollPane.appendChild(this.view);
 82     this.canvas.style.position = "absolute";
 83     this.canvas.style.left = "0px";
 84     this.canvas.style.top = "0px";
 85     this.scrollPane.style.position = "absolute";
 86     this.scrollPane.style.left = "0px";
 87     this.scrollPane.style.top = "0px";
 88     this.scrollPane.addEventListener("scroll", function(ev) {
 89         plan.repaint();
 90       });
 91   }
 92 
 93   this.windowResizeListener = function() {
 94       plan.revalidate();
 95     };
 96   window.addEventListener("resize", this.windowResizeListener);
 97   this.tooltip = document.createElement("div");
 98   this.tooltip.style.position = "absolute";
 99   this.tooltip.style.visibility = "hidden";
100   this.tooltip.style.backgroundColor = ColorTools.toRGBAStyle(this.getBackground(), 0.7);
101   this.tooltip.style.borderWidth = "2px";
102   this.tooltip.style.paddingLeft = "2px";
103   this.tooltip.style.borderStyle = "solid";
104   this.tooltip.style.whiteSpace = "nowrap";
105   this.tooltip.style.borderColor = ColorTools.toRGBAStyle(this.getForeground(), 0.7);
106   this.tooltip.style.font = this.font;
107   this.tooltip.style.color = this.canvas.style.color;
108   this.tooltip.style.zIndex = 101;
109   document.body.appendChild(this.tooltip);
110 
111   this.touchOverlay = document.createElement("div");
112   this.touchOverlay.classList.add("touch-overlay-timer");
113   this.touchOverlay.style.position = "absolute";
114   this.touchOverlay.style.top = "0px";
115   this.touchOverlay.style.left = "0px";
116   this.touchOverlay.innerHTML = '<div id="plan-touch-overlay-timer-content" class="touch-overlay-timer-content"></div><div class="touch-overlay-timer-bg"></div><div class="touch-overlay-timer-hidder"></div><div class="touch-overlay-timer-loader1"></div><div class="touch-overlay-timer-loader2"></div>';
117   document.body.appendChild(this.touchOverlay);
118   for (var i = 0; i < this.touchOverlay.children.length; i++) {
119     var item = this.touchOverlay.children.item(i);
120     if (item.classList.contains("overlay-timer-loader1")
121         || item.classList.contains("overlay-timer-loader2")) {
122       item.style.borderTopColor = this.getSelectionColor();
123       item.style.borderRightColor = this.getSelectionColor();
124     }
125     if (item.classList.contains("touch-overlay-timer-content")) {
126       item.style.color = this.getForeground();
127     }
128     item.style.animationDuration = (PlanComponent.LONG_TOUCH_DURATION_AFTER_DELAY) + "ms";
129   }
130 
131   this.resolutionScale = this.scrollPane ? PlanComponent.HIDPI_SCALE_FACTOR : 1.;
132   this.selectedItemsOutlinePainted = true;
133   this.backgroundPainted = true;
134   this.planBoundsCacheValid = false;
135   
136   this.setOpaque(true);
137   this.addModelListeners(home, preferences, controller);
138   if (controller != null) {
139     this.addMouseListeners(controller);
140     this.addFocusListener(controller);
141     this.addControllerListener(controller);
142     this.createActions(controller);
143     this.installDefaultKeyboardActions();
144   }
145 
146   this.rotationCursor = PlanComponent.createCustomCursor('rotation', 'alias');
147   this.elevationCursor = PlanComponent.createCustomCursor('elevation', 'row-resize');
148   this.heightCursor = PlanComponent.createCustomCursor('height', 'ns-resize');
149   this.powerCursor = PlanComponent.createCustomCursor('power', 'cell');
150   this.resizeCursor = PlanComponent.createCustomCursor('resize', 'ew-resize');
151   this.moveCursor = PlanComponent.createCustomCursor('move', 'move');
152   this.panningCursor = PlanComponent.createCustomCursor('panning', 'move');
153   this.duplicationCursor = 'copy';
154 
155   this.patternImagesCache = {};
156   
157   this.setScale(0.5);
158   
159   setTimeout(this.windowResizeListener);
160 }
161 
162 PlanComponent["__interfaces"] = ["com.eteks.sweethome3d.viewcontroller.PlanView", "com.eteks.sweethome3d.viewcontroller.View", "com.eteks.sweethome3d.viewcontroller.ExportableView", "com.eteks.sweethome3d.viewcontroller.TransferableView"];
163 
164 /** 
165  * @private 
166  */
167 PlanComponent.initStatics = function() {
168   PlanComponent.MARGIN = 40;
169   
170   PlanComponent.INDICATOR_STROKE = new java.awt.BasicStroke(1.5);
171   PlanComponent.POINT_STROKE = new java.awt.BasicStroke(2.0);
172   
173   PlanComponent.WALL_STROKE_WIDTH = 1.5;
174   PlanComponent.BORDER_STROKE_WIDTH = 1.0;
175   PlanComponent.ALIGNMENT_LINE_OFFSET = 25;
176   
177   PlanComponent.ERROR_TEXTURE_IMAGE = null;
178   PlanComponent.WAIT_TEXTURE_IMAGE = null;
179   
180   // TODO Generic resolution support (see https://stackoverflow.com/questions/15661339/how-do-i-fix-blurry-text-in-my-html5-canvas)
181   PlanComponent.HIDPI_SCALE_FACTOR = 2;
182 
183   PlanComponent.POINT_INDICATOR = new java.awt.geom.Ellipse2D.Float(-1.5, -1.5, 3, 3);
184   
185   PlanComponent.FURNITURE_ROTATION_INDICATOR = new java.awt.geom.GeneralPath();
186   PlanComponent.FURNITURE_ROTATION_INDICATOR.append(PlanComponent.POINT_INDICATOR, false);
187   PlanComponent.FURNITURE_ROTATION_INDICATOR.append(new java.awt.geom.Arc2D.Float(-8, -8, 16, 16, 45, 180, java.awt.geom.Arc2D.OPEN), false);
188   PlanComponent.FURNITURE_ROTATION_INDICATOR.moveTo(2.66, -5.66);
189   PlanComponent.FURNITURE_ROTATION_INDICATOR.lineTo(5.66, -5.66);
190   PlanComponent.FURNITURE_ROTATION_INDICATOR.lineTo(4.0, -8.3);
191   
192   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR = new java.awt.geom.GeneralPath();
193   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.append(PlanComponent.POINT_INDICATOR, false);
194   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.moveTo(-4.5, 0);
195   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-5.2, 0);
196   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.moveTo(-9.0, 0);
197   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-10, 0);
198   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.append(new java.awt.geom.Arc2D.Float(-12, -8, 5, 16, 200, 320, java.awt.geom.Arc2D.OPEN), false);
199   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.moveTo(-10.0, -4.5);
200   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-12.3, -2.0);
201   PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.lineTo(-12.8, -5.8);
202   
203   var transform = java.awt.geom.AffineTransform.getRotateInstance(-Math.PI / 2);
204   transform.concatenate(java.awt.geom.AffineTransform.getScaleInstance(1, -1));
205   PlanComponent.FURNITURE_ROLL_ROTATION_INDICATOR = PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.createTransformedShape(transform);
206   
207   PlanComponent.ELEVATION_POINT_INDICATOR = new java.awt.geom.Rectangle2D.Float(-1.5, -1.5, 3.0, 3.0);
208   
209   PlanComponent.ELEVATION_INDICATOR = new java.awt.geom.GeneralPath();
210   PlanComponent.ELEVATION_INDICATOR.moveTo(0, -5);
211   PlanComponent.ELEVATION_INDICATOR.lineTo(0, 5);
212   PlanComponent.ELEVATION_INDICATOR.moveTo(-2.5, 5);
213   PlanComponent.ELEVATION_INDICATOR.lineTo(2.5, 5);
214   PlanComponent.ELEVATION_INDICATOR.moveTo(-1.2, 1.5);
215   PlanComponent.ELEVATION_INDICATOR.lineTo(0, 4.5);
216   PlanComponent.ELEVATION_INDICATOR.lineTo(1.2, 1.5);
217   
218   PlanComponent.HEIGHT_POINT_INDICATOR = new java.awt.geom.Rectangle2D.Float(-1.5, -1.5, 3.0, 3.0);
219   
220   PlanComponent.FURNITURE_HEIGHT_INDICATOR = new java.awt.geom.GeneralPath();
221   PlanComponent.FURNITURE_HEIGHT_INDICATOR.moveTo(0, -6);
222   PlanComponent.FURNITURE_HEIGHT_INDICATOR.lineTo(0, 6);
223   PlanComponent.FURNITURE_HEIGHT_INDICATOR.moveTo(-2.5, -6);
224   PlanComponent.FURNITURE_HEIGHT_INDICATOR.lineTo(2.5, -6);
225   PlanComponent.FURNITURE_HEIGHT_INDICATOR.moveTo(-2.5, 6);
226   PlanComponent.FURNITURE_HEIGHT_INDICATOR.lineTo(2.5, 6);
227   PlanComponent.FURNITURE_HEIGHT_INDICATOR.moveTo(-1.2, -2.5);
228   PlanComponent.FURNITURE_HEIGHT_INDICATOR.lineTo(0.0, -5.5);
229   PlanComponent.FURNITURE_HEIGHT_INDICATOR.lineTo(1.2, -2.5);
230   PlanComponent.FURNITURE_HEIGHT_INDICATOR.moveTo(-1.2, 2.5);
231   PlanComponent.FURNITURE_HEIGHT_INDICATOR.lineTo(0.0, 5.5);
232   PlanComponent.FURNITURE_HEIGHT_INDICATOR.lineTo(1.2, 2.5);
233   
234   PlanComponent.LIGHT_POWER_POINT_INDICATOR = new java.awt.geom.Rectangle2D.Float(-1.5, -1.5, 3.0, 3.0);
235   
236   PlanComponent.LIGHT_POWER_INDICATOR = new java.awt.geom.GeneralPath();
237   PlanComponent.LIGHT_POWER_INDICATOR.moveTo(-8, 0);
238   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(-6.0, 0);
239   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(-6.0, -1);
240   PlanComponent.LIGHT_POWER_INDICATOR.closePath();
241   PlanComponent.LIGHT_POWER_INDICATOR.moveTo(-3, 0);
242   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(-1.0, 0);
243   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(-1.0, -2.5);
244   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(-3.0, -1.8);
245   PlanComponent.LIGHT_POWER_INDICATOR.closePath();
246   PlanComponent.LIGHT_POWER_INDICATOR.moveTo(2, 0);
247   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(4, 0);
248   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(4.0, -3.5);
249   PlanComponent.LIGHT_POWER_INDICATOR.lineTo(2.0, -2.8);
250   PlanComponent.LIGHT_POWER_INDICATOR.closePath();
251   
252   PlanComponent.FURNITURE_RESIZE_INDICATOR = new java.awt.geom.GeneralPath();
253   PlanComponent.FURNITURE_RESIZE_INDICATOR.append(new java.awt.geom.Rectangle2D.Float(-1.5, -1.5, 3.0, 3.0), false);
254   PlanComponent.FURNITURE_RESIZE_INDICATOR.moveTo(5, -4);
255   PlanComponent.FURNITURE_RESIZE_INDICATOR.lineTo(7, -4);
256   PlanComponent.FURNITURE_RESIZE_INDICATOR.lineTo(7, 7);
257   PlanComponent.FURNITURE_RESIZE_INDICATOR.lineTo(-4, 7);
258   PlanComponent.FURNITURE_RESIZE_INDICATOR.lineTo(-4, 5);
259   PlanComponent.FURNITURE_RESIZE_INDICATOR.moveTo(3.5, 3.5);
260   PlanComponent.FURNITURE_RESIZE_INDICATOR.lineTo(9, 9);
261   PlanComponent.FURNITURE_RESIZE_INDICATOR.moveTo(7, 9.5);
262   PlanComponent.FURNITURE_RESIZE_INDICATOR.lineTo(10, 10);
263   PlanComponent.FURNITURE_RESIZE_INDICATOR.lineTo(9.5, 7);
264   
265   PlanComponent.WALL_ORIENTATION_INDICATOR = new java.awt.geom.GeneralPath();
266   PlanComponent.WALL_ORIENTATION_INDICATOR.moveTo(-4, -4);
267   PlanComponent.WALL_ORIENTATION_INDICATOR.lineTo(4, 0);
268   PlanComponent.WALL_ORIENTATION_INDICATOR.lineTo(-4, 4);
269   
270   PlanComponent.WALL_POINT = new java.awt.geom.Ellipse2D.Float(-3, -3, 6, 6);
271   
272   PlanComponent.WALL_ARC_EXTENT_INDICATOR = new java.awt.geom.GeneralPath();
273   PlanComponent.WALL_ARC_EXTENT_INDICATOR.append(new java.awt.geom.Arc2D.Float(-4, 1, 8, 5, 210, 120, java.awt.geom.Arc2D.OPEN), false);
274   PlanComponent.WALL_ARC_EXTENT_INDICATOR.moveTo(0, 6);
275   PlanComponent.WALL_ARC_EXTENT_INDICATOR.lineTo(0, 11);
276   PlanComponent.WALL_ARC_EXTENT_INDICATOR.moveTo(-1.8, 8.7);
277   PlanComponent.WALL_ARC_EXTENT_INDICATOR.lineTo(0, 12);
278   PlanComponent.WALL_ARC_EXTENT_INDICATOR.lineTo(1.8, 8.7);
279   
280   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR = new java.awt.geom.GeneralPath();
281   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR.moveTo(5, -2);
282   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR.lineTo(5, 2);
283   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR.moveTo(6, 0);
284   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR.lineTo(11, 0);
285   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR.moveTo(8.7, -1.8);
286   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR.lineTo(12, 0);
287   PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR.lineTo(8.7, 1.8);
288   
289   transform = java.awt.geom.AffineTransform.getRotateInstance(-Math.PI / 4);
290   PlanComponent.CAMERA_YAW_ROTATION_INDICATOR = PlanComponent.FURNITURE_ROTATION_INDICATOR.createTransformedShape(transform);
291   
292   transform = java.awt.geom.AffineTransform.getRotateInstance(Math.PI);
293   PlanComponent.CAMERA_PITCH_ROTATION_INDICATOR = PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR.createTransformedShape(transform);
294   
295   PlanComponent.CAMERA_ELEVATION_INDICATOR = new java.awt.geom.GeneralPath();
296   PlanComponent.CAMERA_ELEVATION_INDICATOR.moveTo(0, -4);
297   PlanComponent.CAMERA_ELEVATION_INDICATOR.lineTo(0, 4);
298   PlanComponent.CAMERA_ELEVATION_INDICATOR.moveTo(-2.5, 4);
299   PlanComponent.CAMERA_ELEVATION_INDICATOR.lineTo(2.5, 4);
300   PlanComponent.CAMERA_ELEVATION_INDICATOR.moveTo(-1.2, 0.5);
301   PlanComponent.CAMERA_ELEVATION_INDICATOR.lineTo(0, 3.5);
302   PlanComponent.CAMERA_ELEVATION_INDICATOR.lineTo(1.2, 0.5);
303   
304   var cameraHumanBodyAreaPath = new java.awt.geom.GeneralPath();
305   cameraHumanBodyAreaPath.append(new java.awt.geom.Ellipse2D.Float(-0.5, -0.425, 1.0, 0.85), false);
306   cameraHumanBodyAreaPath.append(new java.awt.geom.Ellipse2D.Float(-0.5, -0.3, 0.24, 0.6), false);
307   cameraHumanBodyAreaPath.append(new java.awt.geom.Ellipse2D.Float(0.26, -0.3, 0.24, 0.6), false);
308   PlanComponent.CAMERA_HUMAN_BODY = new java.awt.geom.Area(cameraHumanBodyAreaPath);
309   
310   var cameraHumanHeadAreaPath = new java.awt.geom.GeneralPath();
311   cameraHumanHeadAreaPath.append(new java.awt.geom.Ellipse2D.Float(-0.18, -0.45, 0.36, 1.0), false);
312   cameraHumanHeadAreaPath.moveTo(-0.04, 0.55);
313   cameraHumanHeadAreaPath.lineTo(0, 0.65);
314   cameraHumanHeadAreaPath.lineTo(0.04, 0.55);
315   cameraHumanHeadAreaPath.closePath();
316   PlanComponent.CAMERA_HUMAN_HEAD = new java.awt.geom.Area(cameraHumanHeadAreaPath);
317   
318   var cameraBodyAreaPath = new java.awt.geom.GeneralPath();
319   cameraBodyAreaPath.moveTo(0.5, 0.3); 
320   cameraBodyAreaPath.lineTo(0.45, 0.35);
321   cameraBodyAreaPath.lineTo(0.2, 0.35);
322   cameraBodyAreaPath.lineTo(0.2, 0.5);
323   cameraBodyAreaPath.lineTo(-0.2, 0.5);
324   cameraBodyAreaPath.lineTo(-0.2, 0.35);
325   cameraBodyAreaPath.lineTo(-0.3, 0.35);
326   cameraBodyAreaPath.lineTo(-0.35, 0.5);
327   cameraBodyAreaPath.lineTo(-0.5, 0.3);
328   cameraBodyAreaPath.lineTo(-0.5, -0.45);
329   cameraBodyAreaPath.lineTo(-0.45, -0.5);
330   cameraBodyAreaPath.lineTo(0.45, -0.5);
331   cameraBodyAreaPath.lineTo(0.5, -0.45);
332   cameraBodyAreaPath.closePath();
333   PlanComponent.CAMERA_BODY = new java.awt.geom.Area(cameraBodyAreaPath);
334 
335   PlanComponent.CAMERA_BUTTON = new java.awt.geom.Ellipse2D.Float(-0.37, -0.2, 0.15, 0.32);
336 
337   PlanComponent.DIMENSION_LINE_MARK_END = new java.awt.geom.GeneralPath();
338   PlanComponent.DIMENSION_LINE_MARK_END.moveTo(-5, 5);
339   PlanComponent.DIMENSION_LINE_MARK_END.lineTo(5, -5);
340   PlanComponent.DIMENSION_LINE_MARK_END.moveTo(0, 5);
341   PlanComponent.DIMENSION_LINE_MARK_END.lineTo(0, -5);
342   
343   PlanComponent.VERTICAL_DIMENSION_LINE_DISC = new java.awt.geom.Ellipse2D.Float(-1.5, -1.5, 3, 3);
344   PlanComponent.VERTICAL_DIMENSION_LINE = new java.awt.geom.GeneralPath();
345   PlanComponent.VERTICAL_DIMENSION_LINE.append(new java.awt.geom.Ellipse2D.Float(-5, -5, 10, 10), false);
346 
347   PlanComponent.DIMENSION_LINE_HEIGHT_INDICATOR = PlanComponent.FURNITURE_HEIGHT_INDICATOR;
348 
349   PlanComponent.TEXT_LOCATION_INDICATOR = new java.awt.geom.GeneralPath();
350   PlanComponent.TEXT_LOCATION_INDICATOR.append(new java.awt.geom.Arc2D.Float(-2, 0, 4, 4, 190, 160, java.awt.geom.Arc2D.CHORD), false);
351   PlanComponent.TEXT_LOCATION_INDICATOR.moveTo(0, 4);
352   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(0, 12);
353   PlanComponent.TEXT_LOCATION_INDICATOR.moveTo(-1.2, 8.5);
354   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(0.0, 11.5);
355   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(1.2, 8.5);
356   PlanComponent.TEXT_LOCATION_INDICATOR.moveTo(2.0, 3.0);
357   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(9, 6);
358   PlanComponent.TEXT_LOCATION_INDICATOR.moveTo(6, 6.5);
359   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(10, 7);
360   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(7.5, 3.5);
361   PlanComponent.TEXT_LOCATION_INDICATOR.moveTo(-2.0, 3.0);
362   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(-9, 6);
363   PlanComponent.TEXT_LOCATION_INDICATOR.moveTo(-6, 6.5);
364   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(-10, 7);
365   PlanComponent.TEXT_LOCATION_INDICATOR.lineTo(-7.5, 3.5);
366   
367   PlanComponent.TEXT_ANGLE_INDICATOR = new java.awt.geom.GeneralPath();
368   PlanComponent.TEXT_ANGLE_INDICATOR.append(new java.awt.geom.Arc2D.Float(-1.25, -1.25, 2.5, 2.5, 10, 160, java.awt.geom.Arc2D.CHORD), false);
369   PlanComponent.TEXT_ANGLE_INDICATOR.append(new java.awt.geom.Arc2D.Float(-8, -8, 16, 16, 30, 120, java.awt.geom.Arc2D.OPEN), false);
370   PlanComponent.TEXT_ANGLE_INDICATOR.moveTo(4.0, -5.2);
371   PlanComponent.TEXT_ANGLE_INDICATOR.lineTo(6.9, -4.0);
372   PlanComponent.TEXT_ANGLE_INDICATOR.lineTo(5.8, -7.0);
373   
374   PlanComponent.LABEL_CENTER_INDICATOR = new java.awt.geom.Ellipse2D.Float(-1.0, -1.0, 2, 2);
375   
376   PlanComponent.COMPASS_DISC = new java.awt.geom.Ellipse2D.Float(-0.5, -0.5, 1, 1);
377   var stroke = new java.awt.BasicStroke(0.01);
378   PlanComponent.COMPASS = new java.awt.geom.GeneralPath(stroke.createStrokedShape(PlanComponent.COMPASS_DISC));
379   PlanComponent.COMPASS.append(stroke.createStrokedShape(new java.awt.geom.Line2D.Float(-0.6, 0, -0.5, 0)), false);
380   PlanComponent.COMPASS.append(stroke.createStrokedShape(new java.awt.geom.Line2D.Float(0.6, 0, 0.5, 0)), false);
381   PlanComponent.COMPASS.append(stroke.createStrokedShape(new java.awt.geom.Line2D.Float(0, 0.6, 0, 0.5)), false);
382   stroke = new java.awt.BasicStroke(0.04, java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND);
383   PlanComponent.COMPASS.append(stroke.createStrokedShape(new java.awt.geom.Line2D.Float(0, 0, 0, 0)), false);
384   var compassNeedle = new java.awt.geom.GeneralPath();
385   compassNeedle.moveTo(0, -0.47);
386   compassNeedle.lineTo(0.15, 0.46);
387   compassNeedle.lineTo(0, 0.32);
388   compassNeedle.lineTo(-0.15, 0.46);
389   compassNeedle.closePath();
390   stroke = new java.awt.BasicStroke(0.03);
391   PlanComponent.COMPASS.append(stroke.createStrokedShape(compassNeedle), false);
392   var compassNorthDirection = new java.awt.geom.GeneralPath();
393   compassNorthDirection.moveTo(-0.07, -0.55);
394   compassNorthDirection.lineTo(-0.07, -0.69);
395   compassNorthDirection.lineTo(0.07, -0.56);
396   compassNorthDirection.lineTo(0.07, -0.7);
397   PlanComponent.COMPASS.append(stroke.createStrokedShape(compassNorthDirection), false);
398   
399   PlanComponent.COMPASS_ROTATION_INDICATOR = new java.awt.geom.GeneralPath();
400   PlanComponent.COMPASS_ROTATION_INDICATOR.append(PlanComponent.POINT_INDICATOR, false);
401   PlanComponent.COMPASS_ROTATION_INDICATOR.append(new java.awt.geom.Arc2D.Float(-8, -7, 16, 16, 210, 120, java.awt.geom.Arc2D.OPEN), false);
402   PlanComponent.COMPASS_ROTATION_INDICATOR.moveTo(4.0, 5.66);
403   PlanComponent.COMPASS_ROTATION_INDICATOR.lineTo(7.0, 5.66);
404   PlanComponent.COMPASS_ROTATION_INDICATOR.lineTo(5.6, 8.3);
405   
406   transform = java.awt.geom.AffineTransform.getRotateInstance(Math.PI / 2);
407   PlanComponent.DIMENSION_LINE_HEIGHT_ROTATION_INDICATOR = PlanComponent.COMPASS_ROTATION_INDICATOR.createTransformedShape(transform);
408 
409   
410   PlanComponent.COMPASS_RESIZE_INDICATOR = new java.awt.geom.GeneralPath();
411   PlanComponent.COMPASS_RESIZE_INDICATOR.append(new java.awt.geom.Rectangle2D.Float(-1.5, -1.5, 3.0, 3.0), false);
412   PlanComponent.COMPASS_RESIZE_INDICATOR.moveTo(4, -6);
413   PlanComponent.COMPASS_RESIZE_INDICATOR.lineTo(6, -6);
414   PlanComponent.COMPASS_RESIZE_INDICATOR.lineTo(6, 6);
415   PlanComponent.COMPASS_RESIZE_INDICATOR.lineTo(4, 6);
416   PlanComponent.COMPASS_RESIZE_INDICATOR.moveTo(5, 0);
417   PlanComponent.COMPASS_RESIZE_INDICATOR.lineTo(9, 0);
418   PlanComponent.COMPASS_RESIZE_INDICATOR.moveTo(9, -1.5);
419   PlanComponent.COMPASS_RESIZE_INDICATOR.lineTo(12, 0);
420   PlanComponent.COMPASS_RESIZE_INDICATOR.lineTo(9, 1.5);
421   
422   PlanComponent.ARROW = new java.awt.geom.GeneralPath();
423   PlanComponent.ARROW.moveTo(-5, -2);
424   PlanComponent.ARROW.lineTo(0, 0);
425   PlanComponent.ARROW.lineTo(-5, 2);
426   
427   PlanComponent.ERROR_TEXTURE_IMAGE = TextureManager.getInstance().getErrorImage();
428   PlanComponent.WAIT_TEXTURE_IMAGE = TextureManager.getInstance().getWaitImage();
429 
430   PlanComponent.WEBGL_AVAILABLE = true;
431   var canvas = document.createElement("canvas");
432   var gl = canvas.getContext("webgl");
433   if (!gl) {
434     gl = canvas.getContext("experimental-webgl");
435     if (!gl) {
436       PlanComponent.WEBGL_AVAILABLE = false;
437     }
438   }
439   
440   PlanComponent.LONG_TOUCH_DELAY = 200; // ms
441   PlanComponent.LONG_TOUCH_DELAY_WHEN_DRAGGING = 400; // ms
442   PlanComponent.LONG_TOUCH_DURATION_AFTER_DELAY = 800; // ms
443   PlanComponent.DOUBLE_TOUCH_DELAY = 500; // ms
444 }
445 
446 PlanComponent.initStatics();
447 
448 /**
449  * The circumstances under which the home items displayed by this component will be painted.
450  * @enum
451  * @property {PlanComponent.PaintMode} PAINT
452  * @property {PlanComponent.PaintMode} PRINT
453  * @property {PlanComponent.PaintMode} CLIPBOARD
454  * @property {PlanComponent.PaintMode} EXPORT
455  */
456 PlanComponent.PaintMode = {};
457 PlanComponent.PaintMode[PlanComponent.PaintMode["PAINT"] = 0] = "PAINT";
458 PlanComponent.PaintMode[PlanComponent.PaintMode["PRINT"] = 1] = "PRINT";
459 PlanComponent.PaintMode[PlanComponent.PaintMode["CLIPBOARD"] = 2] = "CLIPBOARD";
460 PlanComponent.PaintMode[PlanComponent.PaintMode["EXPORT"] = 3] = "EXPORT";
461 
462 /**
463  * @private
464  */
465 PlanComponent.ActionType = {};
466 PlanComponent.ActionType[PlanComponent.ActionType["DELETE_SELECTION"] = 0] = "DELETE_SELECTION";
467 PlanComponent.ActionType[PlanComponent.ActionType["ESCAPE"] = 1] = "ESCAPE";
468 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_LEFT"] = 2] = "MOVE_SELECTION_LEFT";
469 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_UP"] = 3] = "MOVE_SELECTION_UP";
470 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_DOWN"] = 4] = "MOVE_SELECTION_DOWN";
471 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_RIGHT"] = 5] = "MOVE_SELECTION_RIGHT";
472 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_FAST_LEFT"] = 6] = "MOVE_SELECTION_FAST_LEFT";
473 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_FAST_UP"] = 7] = "MOVE_SELECTION_FAST_UP";
474 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_FAST_DOWN"] = 8] = "MOVE_SELECTION_FAST_DOWN";
475 PlanComponent.ActionType[PlanComponent.ActionType["MOVE_SELECTION_FAST_RIGHT"] = 9] = "MOVE_SELECTION_FAST_RIGHT";
476 PlanComponent.ActionType[PlanComponent.ActionType["TOGGLE_MAGNETISM_ON"] = 10] = "TOGGLE_MAGNETISM_ON";
477 PlanComponent.ActionType[PlanComponent.ActionType["TOGGLE_MAGNETISM_OFF"] = 11] = "TOGGLE_MAGNETISM_OFF";
478 PlanComponent.ActionType[PlanComponent.ActionType["ACTIVATE_ALIGNMENT"] = 12] = "ACTIVATE_ALIGNMENT";
479 PlanComponent.ActionType[PlanComponent.ActionType["DEACTIVATE_ALIGNMENT"] = 13] = "DEACTIVATE_ALIGNMENT";
480 PlanComponent.ActionType[PlanComponent.ActionType["ACTIVATE_DUPLICATION"] = 14] = "ACTIVATE_DUPLICATION";
481 PlanComponent.ActionType[PlanComponent.ActionType["DEACTIVATE_DUPLICATION"] = 15] = "DEACTIVATE_DUPLICATION";
482 PlanComponent.ActionType[PlanComponent.ActionType["ACTIVATE_EDITIION"] = 16] = "ACTIVATE_EDITIION";
483 PlanComponent.ActionType[PlanComponent.ActionType["DEACTIVATE_EDITIION"] = 17] = "DEACTIVATE_EDITIION";
484 
485 /**
486  * Indicator types that may be displayed on selected items.
487  * @constructor
488  */
489 PlanComponent.IndicatorType = function(name) {
490   this.name = name;
491 }
492 
493 /**
494  * @return {string}
495  */
496 PlanComponent.IndicatorType.prototype.name = function() {
497   return this.name;
498 }
499 
500 /**
501  * @return {string}
502  */
503 PlanComponent.IndicatorType.prototype.toString = function() {
504   return this.name;
505 }
506 PlanComponent.IndicatorType.ROTATE = new PlanComponent.IndicatorType("ROTATE");
507 PlanComponent.IndicatorType.RESIZE = new PlanComponent.IndicatorType("RESIZE");
508 PlanComponent.IndicatorType.ELEVATE = new PlanComponent.IndicatorType("ELEVATE");
509 PlanComponent.IndicatorType.RESIZE_HEIGHT = new PlanComponent.IndicatorType("RESIZE_HEIGHT");
510 PlanComponent.IndicatorType.CHANGE_POWER = new PlanComponent.IndicatorType("CHANGE_POWER");
511 PlanComponent.IndicatorType.MOVE_TEXT = new PlanComponent.IndicatorType("MOVE_TEXT");
512 PlanComponent.IndicatorType.ROTATE_TEXT = new PlanComponent.IndicatorType("ROTATE_TEXT");
513 PlanComponent.IndicatorType.ROTATE_PITCH = new PlanComponent.IndicatorType("ROTATE_PITCH");
514 PlanComponent.IndicatorType.ROTATE_ROLL = new PlanComponent.IndicatorType("ROTATE_ROLL");
515 PlanComponent.IndicatorType.ARC_EXTENT = new PlanComponent.IndicatorType("ARC_EXTENT");
516 
517 /**
518  * Returns the HTML element used to view this component at screen.
519  */
520 PlanComponent.prototype.getHTMLElement = function() {
521   return this.container;
522 }
523 
524 /**
525  * Adds home items and selection listeners on this component to receive
526  * changes notifications from home.
527  * @param {Home} home
528  * @param {UserPreferences} preferences
529  * @param {PlanController} controller
530  * @private
531  */
532 PlanComponent.prototype.addModelListeners = function(home, preferences, controller) {
533   var plan = this;
534   var furnitureChangeListener = function(ev) {
535       if (plan.furnitureTopViewIconKeys != null 
536           && ("MODEL" == ev.getPropertyName() 
537               || "MODEL_ROTATION" == ev.getPropertyName() 
538               || "MODEL_FLAGS" == ev.getPropertyName() 
539               || "MODEL_TRANSFORMATIONS" == ev.getPropertyName() 
540               || "ROLL" == ev.getPropertyName() 
541               || "PITCH" == ev.getPropertyName() 
542               || ("WIDTH_IN_PLAN" == ev.getPropertyName() 
543                   || "DEPTH_IN_PLAN" == ev.getPropertyName() 
544                   || "HEIGHT_IN_PLAN" == ev.getPropertyName())
545                  && (ev.getSource().isHorizontallyRotated() 
546                      || ev.getSource().getTexture() != null)
547               || "MODEL_MIRRORED" == ev.getPropertyName() 
548                  && ev.getSource().getRoll() != 0)) {
549         if ("HEIGHT_IN_PLAN" == ev.getPropertyName()) {
550           plan.sortedLevelFurniture = null;
551         }
552         if (!(ev.getSource() instanceof HomeFurnitureGroup)) {
553           if (controller == null || !controller.isModificationState()) {
554             plan.removeTopViewIconFromCache(ev.getSource());
555           } else {
556             if (plan.invalidFurnitureTopViewIcons == null) {
557               plan.invalidFurnitureTopViewIcons = [];
558               var modificationStateListener = function(ev2) {
559                   for (var i = 0; i < plan.invalidFurnitureTopViewIcons.length; i++) {
560                     plan.removeTopViewIconFromCache(plan.invalidFurnitureTopViewIcons[i]);
561                   }
562                   plan.invalidFurnitureTopViewIcons = null;
563                   plan.repaint();
564                   controller.removePropertyChangeListener("MODIFICATION_STATE", modificationStateListener);
565                 };
566               controller.addPropertyChangeListener("MODIFICATION_STATE", modificationStateListener);
567             }
568             if (plan.invalidFurnitureTopViewIcons.indexOf(ev.getSource()) < 0) {
569               plan.invalidFurnitureTopViewIcons.push(ev.getSource());
570             }
571           }
572         }
573         plan.revalidate();
574       } else if (plan.furnitureTopViewIconKeys != null 
575                  && ("PLAN_ICON" == ev.getPropertyName() 
576                      || "COLOR" == ev.getPropertyName() 
577                      || "TEXTURE" == ev.getPropertyName() 
578                      || "MODEL_MATERIALS" == ev.getPropertyName() 
579                      || "SHININESS" == ev.getPropertyName())) {
580         plan.removeTopViewIconFromCache(ev.getSource());
581         plan.repaint();
582       } else if ("ELEVATION" == ev.getPropertyName() 
583                 || "LEVEL" == ev.getPropertyName() 
584                 || "HEIGHT_IN_PLAN" == ev.getPropertyName()) {
585         plan.sortedLevelFurniture = null;
586         plan.repaint();
587       } else if ("ICON" == ev.getPropertyName() 
588                  || "WALL_CUT_OUT_ON_BOTH_SIDES" == ev.getPropertyName()) {
589         plan.repaint();
590       } else if (plan.doorOrWindowWallThicknessAreasCache != null 
591                 && ("WIDTH" == ev.getPropertyName() 
592                     || "DEPTH" == ev.getPropertyName() 
593                     || "ANGLE" == ev.getPropertyName() 
594                     || "MODEL_MIRRORED" == ev.getPropertyName() 
595                     || "X" == ev.getPropertyName() 
596                     || "Y" == ev.getPropertyName() 
597                     || "LEVEL" == ev.getPropertyName()
598                     || "WALL_THICKNESS" == ev.getPropertyName()
599                     || "WALL_DISTANCE" == ev.getPropertyName()
600                     || "WALL_WIDTH" == ev.getPropertyName()
601                     || "WALL_LEFT" == ev.getPropertyName()
602                     || "CUT_OUT_SHAPE" == ev.getPropertyName())
603                  && CoreTools.removeFromMap(plan.doorOrWindowWallThicknessAreasCache, ev.getSource()) != null) {
604         plan.revalidate();
605       } else {
606         plan.revalidate();
607       }
608     };
609   if (home.getFurniture() != null) {
610     home.getFurniture().forEach(function(piece) {
611         piece.addPropertyChangeListener(furnitureChangeListener);
612         if (piece instanceof HomeFurnitureGroup) {
613           piece.getAllFurniture().forEach(function(childPiece) {
614             childPiece.addPropertyChangeListener(furnitureChangeListener);
615           });
616         }
617       });
618   }
619   var furnitureChangeListenerRemover = function(piece) {
620       piece.removePropertyChangeListener(furnitureChangeListener);
621       plan.removeTopViewIconFromCache(piece);
622       if (piece instanceof HomeDoorOrWindow
623           && plan.doorOrWindowWallThicknessAreasCache != null) {
624         CoreTools.removeFromMap(plan.doorOrWindowWallThicknessAreasCache, piece);
625       }
626     };
627   home.addFurnitureListener(function(ev) {
628       var piece = ev.getItem();
629       if (ev.getType() === CollectionEvent.Type.ADD) {
630         piece.addPropertyChangeListener(furnitureChangeListener);
631         if (piece instanceof HomeFurnitureGroup) {
632           piece.getAllFurniture().forEach(function(childPiece) {
633               childPiece.addPropertyChangeListener(furnitureChangeListener);
634             });
635         }
636       } else if (ev.getType() === CollectionEvent.Type.DELETE) {
637         furnitureChangeListenerRemover(piece);
638         if (piece instanceof HomeFurnitureGroup) {
639           piece.getAllFurniture().forEach(function(childPiece) {
640               furnitureChangeListenerRemover(childPiece);
641             });
642         }
643       }
644       plan.sortedLevelFurniture = null;
645       plan.revalidate();
646     });
647   var wallChangeListener = function(ev) {
648       var propertyName = ev.getPropertyName();
649       if ("X_START" == propertyName 
650           || "X_END" == propertyName 
651           || "Y_START" == propertyName 
652           || "Y_END" == propertyName 
653           || "WALL_AT_START" == propertyName 
654           || "WALL_AT_END" == propertyName 
655           || "THICKNESS" == propertyName 
656           || "ARC_EXTENT" == propertyName 
657           || "PATTERN" == propertyName) {
658         if (plan.home.isAllLevelsSelection()) {
659           plan.otherLevelsWallAreaCache = null;
660           plan.otherLevelsWallsCache = null;
661         }
662         plan.wallAreasCache = null;
663         plan.doorOrWindowWallThicknessAreasCache = null;
664         plan.revalidate();
665       } else if ("LEVEL" == propertyName 
666           || "HEIGHT" == propertyName 
667           || "HEIGHT_AT_END" == propertyName) {
668         plan.otherLevelsWallAreaCache = null;
669         plan.otherLevelsWallsCache = null;
670         plan.wallAreasCache = null;
671         plan.repaint();
672       }
673     };
674   if (home.getWalls() != null) {
675     home.getWalls().forEach(function(wall) {
676         wall.addPropertyChangeListener(wallChangeListener);
677       });
678   }
679   home.addWallsListener(function(ev) {
680       if (ev.getType() === CollectionEvent.Type.ADD) {
681         ev.getItem().addPropertyChangeListener(wallChangeListener);
682       } else if (ev.getType() === CollectionEvent.Type.DELETE) {
683         ev.getItem().removePropertyChangeListener(wallChangeListener);
684       }
685       plan.otherLevelsWallAreaCache = null;
686       plan.otherLevelsWallsCache = null;
687       plan.wallAreasCache = null;
688       plan.doorOrWindowWallThicknessAreasCache = null;
689       plan.revalidate();
690     });
691   var roomChangeListener = function(ev) {
692       var propertyName = ev.getPropertyName();
693       if ("POINTS" == propertyName 
694           || "NAME" == propertyName 
695           || "NAME_X_OFFSET" == propertyName 
696           || "NAME_Y_OFFSET" == propertyName 
697           || "NAME_STYLE" == propertyName 
698           || "NAME_ANGLE" == propertyName 
699           || "AREA_VISIBLE" == propertyName 
700           || "AREA_X_OFFSET" == propertyName 
701           || "AREA_Y_OFFSET" == propertyName 
702           || "AREA_STYLE" == propertyName 
703           || "AREA_ANGLE" == propertyName
704           || "FLOOR_VISIBLE" == propertyName
705           || "CEILING_VISIBLE" == propertyName) {
706         plan.sortedLevelRooms = null;
707         plan.otherLevelsRoomAreaCache = null;
708         plan.otherLevelsRoomsCache = null;
709         plan.revalidate();
710       } else if (plan.preferences.isRoomFloorColoredOrTextured() 
711                  && ("FLOOR_COLOR" == propertyName 
712                      || "FLOOR_TEXTURE" == propertyName 
713                      || "FLOOR_VISIBLE" == propertyName)) {
714         plan.repaint();
715       }
716     };
717   if (home.getRooms() != null) {
718     home.getRooms().forEach(function(room) { 
719         return room.addPropertyChangeListener(roomChangeListener); 
720       });
721   }
722   home.addRoomsListener(function(ev) {
723       if (ev.getType() === CollectionEvent.Type.ADD) {
724         ev.getItem().addPropertyChangeListener(roomChangeListener);
725       } else if (ev.getType() === CollectionEvent.Type.DELETE) {
726         ev.getItem().removePropertyChangeListener(roomChangeListener);
727       }
728       plan.sortedLevelRooms = null;
729       plan.otherLevelsRoomAreaCache = null;
730       plan.otherLevelsRoomsCache = null;
731       plan.revalidate();
732     });
733   var changeListener = function(ev) {
734       var propertyName = ev.getPropertyName();
735       if ("COLOR" == propertyName
736           || "DASH_STYLE" == propertyName) {
737         plan.repaint();
738       } else {
739         plan.revalidate();
740       }
741     };
742   if (home.getPolylines() != null) {
743     home.getPolylines().forEach(function(polyline) { 
744         return polyline.addPropertyChangeListener(changeListener); 
745       });
746   }
747   home.addPolylinesListener(function(ev) {
748       if (ev.getType() === CollectionEvent.Type.ADD) {
749         ev.getItem().addPropertyChangeListener(changeListener);
750       } else if (ev.getType() === CollectionEvent.Type.DELETE) {
751         ev.getItem().removePropertyChangeListener(changeListener);
752       }
753       plan.revalidate();
754     });
755   var dimensionLineChangeListener = function(ev) { 
756       var propertyName = ev.getPropertyName();
757       if ("X_START" == propertyName 
758           || "X_END" == propertyName 
759           || "Y_START" == propertyName 
760           || "Y_END" == propertyName 
761           || "ELEVATION_START" == propertyName 
762           || "ELEVATION_END" == propertyName 
763           || "OFFSET" == propertyName 
764           || "END_MARK_SIZE" == propertyName 
765           || "PITCH" == propertyName 
766           || "LENGTH_STYLE" == propertyName) {
767         return plan.revalidate();
768       } else if ("COLOR" == propertyName) {
769         plan.repaint();
770       }
771     };
772   if (home.getDimensionLines() != null) {
773     home.getDimensionLines().forEach(function(dimensionLine) { 
774         return dimensionLine.addPropertyChangeListener(dimensionLineChangeListener); 
775       });
776   }
777   home.addDimensionLinesListener(function(ev) {
778       if (ev.getType() === CollectionEvent.Type.ADD) {
779         ev.getItem().addPropertyChangeListener(dimensionLineChangeListener);
780       } else if (ev.getType() === CollectionEvent.Type.DELETE) {
781         ev.getItem().removePropertyChangeListener(dimensionLineChangeListener);
782       }
783       plan.revalidate();
784     });
785   var labelChangeListener = function(ev) { 
786       return plan.revalidate(); 
787     };
788   if (home.getLabels() != null) {
789     home.getLabels().forEach(function(label) { 
790         return label.addPropertyChangeListener(labelChangeListener); 
791       });
792   }
793   home.addLabelsListener(function(ev) {
794       if (ev.getType() === CollectionEvent.Type.ADD) {
795         ev.getItem().addPropertyChangeListener(labelChangeListener);
796       } else if (ev.getType() === CollectionEvent.Type.DELETE) {
797         ev.getItem().removePropertyChangeListener(labelChangeListener);
798       }
799       plan.revalidate();
800     });
801   var levelChangeListener = function(ev) {
802       var propertyName = ev.getPropertyName();
803       if ("BACKGROUND_IMAGE" == propertyName) {
804         plan.backgroundImageCache = null;
805         plan.revalidate();
806       } else if ("ELEVATION" == propertyName 
807             || "ELEVATION_INDEX" == propertyName 
808             || "VIEWABLE" == propertyName) {
809         plan.clearLevelCache();        
810         plan.repaint();
811       }
812     };
813   if (home.getLevels() != null) {
814     home.getLevels().forEach(function(level) { 
815         return level.addPropertyChangeListener(levelChangeListener); 
816       });
817   }
818   home.addLevelsListener(function(ev) {
819       var level = ev.getItem();
820       if (ev.getType() === CollectionEvent.Type.ADD) {
821         level.addPropertyChangeListener(levelChangeListener);
822       } else if (ev.getType() === CollectionEvent.Type.DELETE) {
823         level.removePropertyChangeListener(levelChangeListener);
824       }
825       plan.revalidate();
826     });
827   home.addPropertyChangeListener("CAMERA", function(ev) { 
828       return plan.revalidate(); 
829     });
830   home.getObserverCamera().addPropertyChangeListener(function(ev) {
831       var propertyName = ev.getPropertyName();
832       if ("X" == propertyName 
833           || "Y" == propertyName 
834           || "FIELD_OF_VIEW" == propertyName 
835           || "YAW" == propertyName 
836           || "WIDTH" == propertyName 
837           || "DEPTH" == propertyName 
838           || "HEIGHT" == propertyName) {
839         plan.revalidate();
840       }
841     });
842   home.getCompass().addPropertyChangeListener(function(ev) {
843       var propertyName = ev.getPropertyName();
844       if ("X" == propertyName 
845           || "Y" == propertyName 
846           || "NORTH_DIRECTION" == propertyName 
847           || "DIAMETER" == propertyName 
848           || "VISIBLE" == propertyName) {
849         plan.revalidate();
850       }
851     });
852   home.addSelectionListener({
853       selectionChanged: function(ev) { 
854         return plan.repaint(); 
855       }
856     });
857   home.addPropertyChangeListener("BACKGROUND_IMAGE", function(ev) {
858       plan.backgroundImageCache = null;
859       plan.repaint();
860     });
861   home.addPropertyChangeListener("SELECTED_LEVEL", function(ev) {
862       plan.clearLevelCache();
863       plan.repaint();
864     });
865   
866   this.preferencesListener = new PlanComponent.UserPreferencesChangeListener(this);
867   preferences.addPropertyChangeListener("UNIT", this.preferencesListener);
868   preferences.addPropertyChangeListener("LANGUAGE", this.preferencesListener);
869   preferences.addPropertyChangeListener("GRID_VISIBLE", this.preferencesListener);
870   preferences.addPropertyChangeListener("DEFAULT_FONT_NAME", this.preferencesListener);
871   preferences.addPropertyChangeListener("FURNITURE_VIEWED_FROM_TOP", this.preferencesListener);
872   preferences.addPropertyChangeListener("FURNITURE_MODEL_ICON_SIZE", this.preferencesListener);
873   preferences.addPropertyChangeListener("ROOM_FLOOR_COLORED_OR_TEXTURED", this.preferencesListener);
874   preferences.addPropertyChangeListener("WALL_PATTERN", this.preferencesListener);
875 }
876 
877 /**
878  * Preferences property listener bound to this component with a weak reference to avoid
879  * strong link between preferences and this component.
880  * @param {PlanComponent} planComponent
881  * @constructor
882  * @private
883  */
884 PlanComponent.UserPreferencesChangeListener = function(planComponent) {
885   this.planComponent = planComponent;
886 }
887 
888 PlanComponent.UserPreferencesChangeListener.prototype.propertyChange = function(ev) {
889   var planComponent = this.planComponent;
890   var preferences = ev.getSource();
891   var property = ev.getPropertyName();
892   if (planComponent == null) {
893     preferences.removePropertyChangeListener(property, this);
894   } else {
895     switch ((property)) {
896     case "LANGUAGE":
897     case "UNIT":
898       if (planComponent.horizontalRuler != null) {
899         planComponent.horizontalRuler.repaint();
900       }
901       if (planComponent.verticalRuler != null) {
902         planComponent.verticalRuler.repaint();
903       }
904       break;
905     case "DEFAULT_FONT_NAME":
906       planComponent.fonts = null;
907       planComponent.fontsMetrics = null;
908       planComponent.revalidate();
909       break;
910     case "WALL_PATTERN":
911       planComponent.wallAreasCache = null;
912       break;
913     case "FURNITURE_VIEWED_FROM_TOP":
914       if (planComponent.furnitureTopViewIconKeys != null && !preferences.isFurnitureViewedFromTop()) {
915         planComponent.furnitureTopViewIconKeys = null;
916         planComponent.furnitureTopViewIconsCache = null;
917       }
918       break;
919     case "FURNITURE_MODEL_ICON_SIZE":
920       planComponent.furnitureTopViewIconKeys = null;
921       planComponent.furnitureTopViewIconsCache = null;
922       break;
923     default:
924       break;
925     }
926     planComponent.repaint();
927   }
928 }
929 
930 /**
931  * Removes piece from maps handling top view icons.
932  * @private 
933  */
934 PlanComponent.prototype.removeTopViewIconFromCache = function(piece) {
935   if (this.furnitureTopViewIconKeys != null) {
936     // Explicitely remove deleted object from some maps since there's no WeakHashMap
937     var topViewIconKey = CoreTools.removeFromMap(this.furnitureTopViewIconKeys, piece);
938     if (topViewIconKey != null) {
939       // Update furnitureTopViewIconsCache too if topViewIconKey isn't used anymore
940       var keys = CoreTools.valuesFromMap(this.furnitureTopViewIconKeys);
941       var removedKeyFound = false; 
942       for (var i = 0; i < keys.length; i++) {
943         if (keys [i].hashCode === topViewIconKey.hashCode // TODO Why prototype is lost? 
944             && keys [i].equals(topViewIconKey)) {
945           removedKeyFound = true;
946         }
947       }
948       if (!removedKeyFound) {
949         CoreTools.removeFromMap(this.furnitureTopViewIconsCache, topViewIconKey);
950       }
951     }
952   }
953 }
954 
955 /**
956  * Clears the cached information bound to level.
957  * @private 
958  */
959 PlanComponent.prototype.clearLevelCache = function() {
960   this.backgroundImageCache = null;
961   this.otherLevelsWallAreaCache = null;
962   this.otherLevelsWallsCache = null;
963   this.otherLevelsRoomAreaCache = null;
964   this.otherLevelsRoomsCache = null;
965   this.wallAreasCache = null;
966   this.doorOrWindowWallThicknessAreasCache = null;
967   this.sortedLevelRooms = null;
968   this.sortedLevelFurniture = null;
969 }
970 
971 /** 
972  * @private 
973  */
974 PlanComponent.prototype.isEnabled = function() {
975   return true;
976 }
977 
978 PlanComponent.prototype.revalidate = function() {
979   this.invalidate(true);
980   this.validate();
981   this.repaint();
982 }
983 
984 /** 
985  * @private 
986  */
987 PlanComponent.prototype.invalidate = function(invalidatePlanBoundsCache) {
988   if (invalidatePlanBoundsCache) {
989     var planBoundsCacheWereValid = this.planBoundsCacheValid;
990     if (!this.invalidPlanBounds) {
991       this.invalidPlanBounds = this.getPlanBounds().getBounds2D();
992     }
993     if (planBoundsCacheWereValid) {
994       this.planBoundsCacheValid = false;
995     }
996   }
997 }
998 
999 /** 
1000  * @private 
1001  */
1002 PlanComponent.prototype.validate = function() {
1003   if (this.invalidPlanBounds != null) {
1004     var size = this.getPreferredSize();
1005     if (this.isScrolled()) {
1006       this.view.style.width = size.width + "px";
1007       this.view.style.height = size.height + "px";
1008       if (this.canvas.width !== this.scrollPane.clientWidth * this.resolutionScale 
1009           || this.canvas.height !== this.scrollPane.clientHeight * this.resolutionScale) {
1010         this.canvas.width = this.scrollPane.clientWidth * this.resolutionScale;
1011         this.canvas.height = this.scrollPane.clientHeight * this.resolutionScale;
1012         this.canvas.style.width = this.scrollPane.clientWidth + "px";
1013         this.canvas.style.height = this.scrollPane.clientHeight + "px";
1014       }
1015 
1016       var planBoundsNewMinX = this.getPlanBounds().getMinX();
1017       var planBoundsNewMinY = this.getPlanBounds().getMinY();
1018       // If plan bounds upper left corner diminished
1019       if (planBoundsNewMinX < this.invalidPlanBounds.getMinX()
1020           || planBoundsNewMinY < this.invalidPlanBounds.getMinY()) {
1021         // Update view position when scroll bars are visible
1022         if (this.scrollPane.clientWidth < this.view.clientWidth
1023             || this.scrollPane.clientHeight < this.view.clientHeight.height) {
1024           var deltaX = this.convertLengthToPixel(this.invalidPlanBounds.getMinX() - planBoundsNewMinX);
1025           var deltaY = this.convertLengthToPixel(this.invalidPlanBounds.getMinY() - planBoundsNewMinY);
1026           this.scrollPane.scrollLeft += deltaX;
1027           this.scrollPane.scrollTop += deltaY;
1028         }
1029       }
1030     } else if (this.canvas.width !== this.canvas.clientWidth 
1031         || this.canvas.height !== this.canvas.clientHeight) {
1032       this.canvas.width = this.canvas.clientWidth; 
1033       this.canvas.height = this.canvas.clientHeight;
1034     }
1035   }
1036   delete this.invalidPlanBounds;
1037 }
1038 
1039 /** 
1040  * @private 
1041  */
1042 PlanComponent.prototype.isScrolled = function() {
1043   return this.scrollPane !== undefined;
1044 }
1045 
1046 /**
1047  * Adds mouse listeners to this component that calls back <code>controller</code> methods.
1048  * @param {PlanController} controller
1049  * @private
1050  */
1051 PlanComponent.prototype.addMouseListeners = function(controller) {
1052   var plan = this;
1053   var mouseListener = {
1054       initialPointerLocation: null,
1055       lastPointerLocation: null,
1056       touchEventType : false,
1057       pointerTouches : {},
1058       lastEventType : null,
1059       lastTargetTouches : [],
1060       distanceLastPinch: null,
1061       panningAfterPinch: false,
1062       firstTouchStartedTimeStamp: 0,
1063       longTouchStartTime: 0,
1064       autoScroll: null,
1065       longTouch: null,
1066       longTouchWhenDragged: false,
1067       actionStartedInPlanComponent: false,
1068       contextMenuEventType: false,
1069       mousePressed: function(ev) {
1070         if (!mouseListener.touchEventType
1071             && !mouseListener.contextMenuEventType
1072             && plan.isEnabled() && ev.button === 0) {
1073           mouseListener.updateCoordinates(ev, "mousePressed");
1074           mouseListener.autoScroll = null;
1075           mouseListener.initialPointerLocation = [ev.canvasX, ev.canvasY];
1076           mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1077           mouseListener.actionStartedInPlanComponent = true;
1078           controller.pressMouse(plan.convertXPixelToModel(ev.canvasX), plan.convertYPixelToModel(ev.canvasY), 
1079               ev.clickCount, mouseListener.isShiftDown(ev), mouseListener.isAlignmentActivated(ev), 
1080               mouseListener.isDuplicationActivated(ev), mouseListener.isMagnetismToggled(ev));
1081         }
1082         ev.stopPropagation();
1083       },
1084       isShiftDown : function(ev) {
1085         return ev.shiftKey && !ev.altKey && !ev.ctrlKey && !ev.metaKey;
1086       }, 
1087       isAlignmentActivated : function(ev) {
1088         return OperatingSystem.isWindows() || OperatingSystem.isMacOSX() 
1089             ? ev.shiftKey 
1090             : ev.shiftKey && !ev.altKey;
1091       }, 
1092       isDuplicationActivated : function(ev) {
1093         return OperatingSystem.isMacOSX() 
1094             ? ev.altKey 
1095             : ev.ctrlKey;
1096       }, 
1097       isMagnetismToggled : function(ev) {
1098         return OperatingSystem.isWindows() 
1099             ? ev.altKey 
1100             : (OperatingSystem.isMacOSX() 
1101                 ? ev.metaKey 
1102                 : ev.shiftKey && ev.altKey);
1103       },
1104       mouseDoubleClicked: function(ev) {
1105         mouseListener.updateCoordinates(ev, "mouseDoubleClicked");
1106         mouseListener.mousePressed(ev);
1107       },
1108       windowMouseMoved: function(ev) {
1109         if (!mouseListener.touchEventType
1110             && !mouseListener.contextMenuEventType) {
1111           mouseListener.updateCoordinates(ev, "mouseMoved");
1112           // Handle autoscroll
1113           if (mouseListener.lastPointerLocation != null) {
1114             if (mouseListener.autoScroll == null 
1115                 && !mouseListener.isInCanvas(ev)) {
1116               mouseListener.autoScroll = setInterval(function() {
1117                   if (mouseListener.actionStartedInPlanComponent) {
1118                     // Dispatch a copy of event (IE doesn't support dispatching with the same event)
1119                     var ev2 = document.createEvent("Event");
1120                     ev2.initEvent("mousemove", true, true);
1121                     ev2.clientX = ev.clientX;
1122                     ev2.clientY = ev.clientY;
1123                     window.dispatchEvent(ev2);
1124                   } else {
1125                     clearInterval(mouseListener.autoScroll);
1126                     mouseListener.autoScroll = null;
1127                   }
1128                 }, 10);
1129             }
1130             if (mouseListener.autoScroll != null 
1131                 && mouseListener.isInCanvas(ev)) {
1132               clearInterval(mouseListener.autoScroll);
1133               mouseListener.autoScroll = null;
1134             }
1135             mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1136           }
1137           
1138           if (mouseListener.initialPointerLocation != null 
1139               && !(mouseListener.initialPointerLocation[0] === ev.canvasX 
1140                   && mouseListener.initialPointerLocation[1] === ev.canvasY)) {
1141             mouseListener.initialPointerLocation = null;
1142           }
1143           if (mouseListener.initialPointerLocation == null
1144               && (ev.buttons === 0 && mouseListener.isInCanvas(ev) 
1145                   || mouseListener.actionStartedInPlanComponent)) {
1146             if (plan.isEnabled()) { 
1147               controller.moveMouse(plan.convertXPixelToModel(ev.canvasX), plan.convertYPixelToModel(ev.canvasY));
1148             }
1149           }
1150         }
1151       }, 
1152       windowMouseReleased: function(ev) {
1153         if (!mouseListener.touchEventType) {
1154           if (mouseListener.lastPointerLocation != null) {
1155             // Stop autoscroll
1156             if (mouseListener.autoScroll != null) {
1157               clearInterval(mouseListener.autoScroll);
1158               mouseListener.autoScroll = null;
1159             }
1160             
1161             if (mouseListener.actionStartedInPlanComponent 
1162                 && plan.isEnabled() && ev.button === 0) {
1163               if (mouseListener.contextMenuEventType) {
1164                 controller.releaseMouse(plan.convertXPixelToModel(mouseListener.initialPointerLocation[0]), 
1165                     plan.convertYPixelToModel(mouseListener.initialPointerLocation[1]));
1166               } else {
1167                 mouseListener.updateCoordinates(ev, "mouseReleased");
1168                 controller.releaseMouse(plan.convertXPixelToModel(ev.canvasX), plan.convertYPixelToModel(ev.canvasY));
1169               }
1170             }
1171             mouseListener.initialPointerLocation = null;
1172             mouseListener.lastPointerLocation = null;
1173             mouseListener.actionStartedInPlanComponent = false;
1174           }
1175         } 
1176         mouseListener.contextMenuEventType = false;
1177       },
1178       pointerPressed : function(ev) {
1179         if (ev.pointerType == "mouse") {
1180           mouseListener.mousePressed(ev);
1181         } else {
1182           // Multi touch support for IE and Edge
1183           mouseListener.copyPointerToTargetTouches(ev, false);
1184           mouseListener.touchStarted(ev);
1185         }
1186       },
1187       pointerMousePressed : function(ev) {
1188         // Required to avoid click simulation
1189         ev.stopPropagation();
1190       },
1191       windowPointerMoved : function(ev) {
1192         if (ev.pointerType == "mouse") {
1193           mouseListener.windowMouseMoved(ev);
1194         } else {
1195           // Multi touch support for IE and Edge
1196           mouseListener.copyPointerToTargetTouches(ev, false) 
1197           mouseListener.touchMoved(ev);
1198         }
1199       },
1200       windowPointerReleased : function(ev) {
1201         if (ev.pointerType == "mouse") {
1202           mouseListener.windowMouseReleased(ev);
1203         } else {
1204           ev.preventDefault();
1205           // Multi touch support for IE and legacy Edge
1206           mouseListener.copyPointerToTargetTouches(ev, true);
1207           mouseListener.touchEnded(ev);
1208         }
1209       },
1210       contextMenuDisplayed : function(ev) {
1211         mouseListener.contextMenuEventType = true;
1212       },
1213       touchStarted: function(ev) {
1214         // Do not prevent default behavior to ensure focus events will be fired if focus changed after a touch event
1215         // but track touch event types to avoid them to be managed also for mousedown and dblclick events
1216         mouseListener.touchEventType = ev.pointerType === undefined;
1217         plan.lastTouchEndX = undefined;
1218         plan.lastTouchEndY = undefined;
1219         if (plan.isEnabled()) {
1220           // Prevent default behavior to ensure a second touchstart event will be received 
1221           // for double taps under iOS >= 15
1222           ev.preventDefault(); 
1223           if (document.activeElement != plan.container) {
1224             // Request focus explicitly since default behavior is disabled
1225             plan.container.focus();
1226           } 
1227           mouseListener.updateCoordinates(ev, "touchStarted");
1228           mouseListener.autoScroll = null;
1229           if (mouseListener.longTouch != null) {
1230             clearTimeout(mouseListener.longTouch);
1231             mouseListener.longTouch = null;
1232             plan.stopLongTouchAnimation();
1233           }
1234 
1235           if (ev.targetTouches.length === 1) {
1236             var clickCount = 1;
1237             if (mouseListener.initialPointerLocation != null
1238                 && mouseListener.distance(ev.canvasX, ev.canvasY,
1239                     mouseListener.initialPointerLocation [0], mouseListener.initialPointerLocation [1]) < 5 
1240                 && ev.timeStamp - mouseListener.firstTouchStartedTimeStamp <= PlanComponent.DOUBLE_TOUCH_DELAY) { 
1241               clickCount = 2;
1242               mouseListener.firstTouchStartedTimeStamp = 0;
1243               mouseListener.initialPointerLocation = null;
1244             } else {
1245               mouseListener.firstTouchStartedTimeStamp = ev.timeStamp;
1246               mouseListener.initialPointerLocation = [ev.canvasX, ev.canvasY];
1247             }
1248                 
1249             mouseListener.distanceLastPinch = null;
1250             mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1251             mouseListener.actionStartedInPlanComponent = true;
1252             mouseListener.longTouchWhenDragged = false;
1253             if (controller.getMode() !== PlanController.Mode.PANNING
1254                 && clickCount == 1) {
1255               var character = controller.getMode() === PlanController.Mode.SELECTION
1256                   ? '⇪'
1257                   : (controller.getMode() === PlanController.Mode.POLYLINE_CREATION
1258                       && !controller.isModificationState()
1259                         ? 'S' : '2');
1260               mouseListener.longTouch = setTimeout(function() {
1261                 plan.startLongTouchAnimation(ev.canvasX, ev.canvasY, character,
1262                     function() {
1263                       if (controller.getMode() === PlanController.Mode.SELECTION) {
1264                         // Simulate shift key press
1265                         controller.setAlignmentActivated(true);
1266                       } else if (controller.getMode() === PlanController.Mode.POLYLINE_CREATION) { 
1267                         // Enable curved or elevation dimension creation 
1268                         controller.setDuplicationActivated(true);
1269                       }
1270                     });
1271                   }, PlanComponent.LONG_TOUCH_DELAY);
1272               mouseListener.longTouchStartTime = Date.now();
1273             }
1274             
1275             controller.pressMouse(plan.convertXPixelToModel(ev.canvasX), plan.convertYPixelToModel(ev.canvasY), 
1276                 clickCount, mouseListener.isShiftDown(ev), mouseListener.isAlignmentActivated(ev), 
1277                 mouseListener.isDuplicationActivated(ev), mouseListener.isMagnetismToggled(ev), View.PointerType.TOUCH);
1278           } else {
1279             // Cancel autoscroll
1280             if (mouseListener.autoScroll != null) {
1281               clearInterval(mouseListener.autoScroll);
1282               mouseListener.autoScroll = null;
1283             }            
1284             // Additional touch allows to escape current modification 
1285             controller.escape();
1286             
1287             if (ev.targetTouches.length === 2) {
1288               mouseListener.actionStartedInPlanComponent = true;
1289               mouseListener.initialPointerLocation = null;
1290               mouseListener.distanceLastPinch = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 
1291                   ev.targetTouches[1].clientX, ev.targetTouches[1].clientY);
1292             }
1293           }
1294         }
1295       },
1296       touchMoved: function(ev) {
1297         if (mouseListener.actionStartedInPlanComponent
1298             && plan.isEnabled()) {
1299           ev.preventDefault();
1300           ev.stopPropagation();
1301           if (mouseListener.updateCoordinates(ev, "touchMoved")) {
1302             plan.stopIndicatorAnimation();            
1303 
1304             mouseListener.initialPointerLocation = null;
1305             
1306             if (ev.targetTouches.length == 1) {
1307               // Handle autoscroll
1308               if (mouseListener.lastPointerLocation != null) {
1309                 if (mouseListener.autoScroll != null 
1310                     && mouseListener.isInCanvas(ev)) {
1311                   clearInterval(mouseListener.autoScroll);
1312                   mouseListener.autoScroll = null;
1313                 }
1314                 if (mouseListener.autoScroll == null 
1315                     && !mouseListener.isInCanvas(ev)
1316                     && controller.getMode() !== PlanController.Mode.PANNING
1317                     && mouseListener.lastPointerLocation != null) {
1318                   mouseListener.autoScroll = setInterval(function() {
1319                       if (mouseListener.actionStartedInPlanComponent) {
1320                         mouseListener.touchMoved(ev);
1321                       } else {
1322                         clearInterval(mouseListener.autoScroll);
1323                         mouseListener.autoScroll = null;
1324                       }
1325                     }, 10);
1326                 }
1327               }
1328               
1329               if (mouseListener.longTouch != null) {
1330                 // Cancel long touch animation only when pointer moved during the past 200 ms
1331                 clearTimeout(mouseListener.longTouch);
1332                 mouseListener.longTouch = null;
1333                 plan.stopLongTouchAnimation();
1334               }
1335               
1336               mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY];
1337               controller.moveMouse(plan.convertXPixelToModel(ev.canvasX), plan.convertYPixelToModel(ev.canvasY));
1338               
1339               if (!mouseListener.autoScroll 
1340                   && controller.getMode() !== PlanController.Mode.PANNING
1341                   && controller.getMode() !== PlanController.Mode.SELECTION) {
1342                 mouseListener.longTouch = setTimeout(function() {
1343                     mouseListener.longTouchWhenDragged = true;   
1344                     plan.startLongTouchAnimation(ev.canvasX, ev.canvasY, '2');
1345                   }, PlanComponent.LONG_TOUCH_DELAY_WHEN_DRAGGING);
1346                 mouseListener.longTouchStartTime = Date.now();
1347               }
1348             } else if (ev.targetTouches.length == 2
1349                 && mouseListener.distanceLastPinch != null) {
1350               var newDistance = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 
1351                   ev.targetTouches[1].clientX, ev.targetTouches[1].clientY);
1352               var scaleDifference = newDistance / mouseListener.distanceLastPinch;
1353               var rect = plan.canvas.getBoundingClientRect();
1354               var x = plan.convertXPixelToModel((ev.targetTouches[0].clientX + ev.targetTouches[1].clientX) / 2 - rect.left);
1355               var y = plan.convertYPixelToModel((ev.targetTouches[0].clientY + ev.targetTouches[1].clientY) / 2 - rect.top);
1356               var oldScale = plan.getScale();
1357               controller.zoom(scaleDifference);
1358               mouseListener.distanceLastPinch = newDistance;
1359               if (plan.isScrolled() 
1360                   && plan.getScale() !== oldScale) {
1361                 // If scale changed, update viewport position to keep the same coordinates under mouse cursor
1362                 plan.scrollPane.scrollLeft = 0;
1363                 plan.scrollPane.scrollTop = 0;
1364                 var mouseDeltaX = (ev.targetTouches[0].clientX + ev.targetTouches[1].clientX) / 2 - rect.left - plan.convertXModelToPixel(x);
1365                 var mouseDeltaY = (ev.targetTouches[0].clientY + ev.targetTouches[1].clientY) / 2 - rect.top - plan.convertYModelToPixel(y);
1366                 plan.moveView(-plan.convertPixelToLength(mouseDeltaX), -plan.convertPixelToLength(mouseDeltaY));
1367               }
1368             }
1369           }
1370         }
1371       },
1372       touchEnded: function(ev) {
1373         if (mouseListener.actionStartedInPlanComponent 
1374             && plan.isEnabled()) {
1375           mouseListener.updateCoordinates(ev, "touchEnded");
1376 
1377           if (mouseListener.panningAfterPinch) {
1378             controller.setMode(PlanController.Mode.SELECTION);
1379             mouseListener.panningAfterPinch = false;
1380           }
1381           
1382           if (ev.targetTouches.length == 0) {
1383             // Cancel autoscroll
1384             if (mouseListener.autoScroll != null) {
1385               clearInterval(mouseListener.autoScroll);
1386               mouseListener.autoScroll = null;
1387             }
1388           
1389             if (mouseListener.longTouch != null) {
1390               clearTimeout(mouseListener.longTouch);
1391               mouseListener.longTouch = null;
1392               plan.stopLongTouchAnimation();
1393             }
1394           
1395             var xModel = plan.convertXPixelToModel(mouseListener.lastPointerLocation [0]);
1396             var yModel = plan.convertYPixelToModel(mouseListener.lastPointerLocation [1]);
1397             controller.releaseMouse(xModel, yModel);
1398             if (controller.getMode() !== PlanController.Mode.SELECTION) {
1399               if (mouseListener.isLongTouch(true)
1400                   && mouseListener.longTouchWhenDragged) {
1401                 // Emulate double click
1402                 controller.pressMouse(xModel, yModel, 1, false, false, false, false, View.PointerType.TOUCH);
1403                 controller.releaseMouse(xModel, yModel);
1404                 controller.pressMouse(xModel, yModel, 2, false, false, false, false, View.PointerType.TOUCH);
1405                 controller.releaseMouse(xModel, yModel);
1406               } else if (mouseListener.isLongTouch()
1407                   && mouseListener.initialPointerLocation != null) {
1408                 // Emulate double click
1409                 controller.pressMouse(xModel, yModel, 2, false, false, false, false, View.PointerType.TOUCH);
1410                 controller.releaseMouse(xModel, yModel);
1411               }
1412             }
1413             
1414             if (mouseListener.isLongTouch()) {
1415               // Avoid firing contextmenu event
1416               ev.preventDefault();
1417             }
1418             plan.stopIndicatorAnimation();
1419             mouseListener.actionStartedInPlanComponent = false;
1420           } else if (ev.targetTouches.length == 1) {
1421             if (controller.getMode() === PlanController.Mode.SELECTION) {
1422               controller.setMode(PlanController.Mode.PANNING);
1423               controller.pressMouse(plan.convertXPixelToModel(ev.canvasX), 
1424                   plan.convertYPixelToModel(ev.canvasY), 1, false, false, false, false, View.PointerType.TOUCH);
1425               mouseListener.panningAfterPinch = true;
1426             }
1427           } else if (ev.targetTouches.length == 2
1428                      && mouseListener.distanceLastPinch != null) {
1429             // If the user keeps 2 finger on screen after releasing other fingers 
1430             mouseListener.distanceLastPinch = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 
1431                 ev.targetTouches[1].clientX, ev.targetTouches[1].clientY)
1432           }
1433           
1434           plan.lastTouchEndX = plan.lastTouchX;
1435           plan.lastTouchEndY = plan.lastTouchY; 
1436           plan.lastTouchX = undefined;
1437           plan.lastTouchY = undefined;
1438         }
1439         // Reset mouseListener.touchEventType in windowMouseReleased call
1440       },
1441       copyPointerToTargetTouches : function(ev, touchEnded) {
1442         // Copy the IE and Edge pointer location to ev.targetTouches or ev.changedTouches
1443         if (touchEnded) {
1444           ev.changedTouches = [mouseListener.pointerTouches [ev.pointerId]];
1445           delete mouseListener.pointerTouches [ev.pointerId];
1446         } else {
1447           mouseListener.pointerTouches [ev.pointerId] = {clientX : ev.clientX, clientY : ev.clientY};
1448         }
1449         ev.targetTouches = [];
1450         for (var attribute in mouseListener.pointerTouches) {
1451           if (mouseListener.pointerTouches.hasOwnProperty(attribute)) {
1452             ev.targetTouches.push(mouseListener.pointerTouches [attribute]);
1453           }
1454         }
1455       },
1456       updateCoordinates : function(ev, type) {
1457         // Updates canvasX and canvasY properties and return true if they changed
1458         plan.lastTouchX = undefined;
1459         plan.lastTouchY = undefined;
1460         var rect = plan.canvas.getBoundingClientRect();
1461         var updated = true; 
1462         if (type.indexOf("touch") === 0) {
1463           plan.pointerType = View.PointerType.TOUCH;
1464           var minDistance = mouseListener.lastEventType == "touchStarted"
1465               ? 5 : 1.5;
1466           var touches;
1467           if (ev.targetTouches.length === 1
1468                 && type == "touchMoved" 
1469                 && mouseListener.distance(mouseListener.lastTargetTouches [0].clientX, mouseListener.lastTargetTouches [0].clientY,
1470                     ev.targetTouches[0].clientX, ev.targetTouches[0].clientY) < minDistance
1471               || ev.targetTouches.length === 0
1472                      && type == "touchEnded" 
1473                      && mouseListener.distance(mouseListener.lastTargetTouches [0].clientX, mouseListener.lastTargetTouches [0].clientY,
1474                          ev.changedTouches[0].clientX, ev.changedTouches[0].clientY) < minDistance) {
1475             touches = mouseListener.lastTargetTouches;
1476             updated = false;
1477           } else {
1478             if (ev.targetTouches.length == 0) {
1479               // touchend case
1480               touches = ev.changedTouches;
1481             } else {
1482               touches = ev.targetTouches;
1483             }
1484             mouseListener.lastEventType = type;
1485           }
1486           
1487           if (touches.length == 1) {
1488             ev.canvasX = touches[0].clientX - rect.left;
1489             ev.canvasY = touches[0].clientY - rect.top;
1490             plan.lastTouchX = ev.canvasX;
1491             plan.lastTouchY = ev.canvasY;
1492           } 
1493           ev.clickCount = 1;
1494 
1495           if (updated) {
1496             // Make a copy of touches because old iOS reuse the same ev.targetTouches array between events
1497             mouseListener.lastTargetTouches = [];
1498             for (var i = 0; touches[i] !== undefined; i++) {
1499               mouseListener.lastTargetTouches.push({clientX: touches[i].clientX, clientY: touches[i].clientY});
1500             }
1501           }
1502         } else {
1503           plan.pointerType = View.PointerType.MOUSE;
1504           ev.canvasX = ev.clientX - rect.left;
1505           ev.canvasY = ev.clientY - rect.top;
1506         }  
1507         
1508         if (ev.clickCount === undefined) {
1509           if (type == "mouseDoubleClicked") {
1510             ev.clickCount = 2;
1511           } else if (type == "mousePressed" || type == "mouseReleased") {
1512             ev.clickCount = 1;
1513           } else {
1514             ev.clickCount = 0;
1515           }
1516         }
1517         if (type == "mouseWheelMoved") {
1518           ev.wheelRotation = (ev.deltaY !== undefined 
1519               ? ev.deltaX + ev.deltaY 
1520               : -ev.wheelDelta) / 4;
1521         }
1522         
1523         return updated;
1524       },
1525       isLongTouch: function(dragging) {
1526         return Date.now() - mouseListener.longTouchStartTime 
1527             > ((dragging 
1528                 ? PlanComponent.LONG_TOUCH_DELAY_WHEN_DRAGGING 
1529                 : PlanComponent.LONG_TOUCH_DELAY) + PlanComponent.LONG_TOUCH_DURATION_AFTER_DELAY);
1530       },
1531       distance: function(x1, y1, x2, y2) {
1532         return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
1533       },
1534       isInCanvas: function(ev) {
1535         return ev.canvasX >= 0 && ev.canvasX < plan.canvas.clientWidth
1536             && ev.canvasY >= 0 && ev.canvasY < plan.canvas.clientHeight;
1537       },
1538       mouseWheelMoved: function(ev) {
1539         ev.preventDefault();
1540         mouseListener.updateCoordinates(ev, "mouseWheelMoved");
1541         var shortcutKeyPressed = OperatingSystem.isMacOSX() ? ev.metaKey : ev.ctrlKey;
1542         if (shortcutKeyPressed) {
1543           var x = plan.convertXPixelToModel(ev.canvasX);
1544           var y = plan.convertYPixelToModel(ev.canvasY);
1545           var oldScale = plan.getScale();
1546           controller.zoom(ev.wheelRotation < 0 
1547               ? Math.pow(1.05, -ev.wheelRotation) 
1548               : Math.pow(0.95, ev.wheelRotation));
1549           if (plan.isScrolled()
1550               && plan.getScale() !== oldScale) {
1551             // If scale changed, update viewport position to keep the same coordinates under mouse cursor
1552             plan.scrollPane.scrollLeft = 0;
1553             plan.scrollPane.scrollTop = 0;
1554             var mouseDeltaX = ev.canvasX - plan.convertXModelToPixel(x);
1555             var mouseDeltaY = ev.canvasY - plan.convertYModelToPixel(y);
1556             plan.moveView(-plan.convertPixelToLength(mouseDeltaX), -plan.convertPixelToLength(mouseDeltaY));
1557           }
1558         } else {
1559           plan.moveView(ev.shiftKey ? plan.convertPixelToLength(ev.wheelRotation) : 0, 
1560                         ev.shiftKey ? 0 : plan.convertPixelToLength(ev.wheelRotation));
1561         }
1562       }
1563     };
1564   if (OperatingSystem.isInternetExplorerOrLegacyEdge()
1565       && window.PointerEvent) {
1566     // Multi touch support for IE and Edge
1567     this.canvas.addEventListener("pointerdown", mouseListener.pointerPressed);
1568     this.canvas.addEventListener("mousedown", mouseListener.pointerMousePressed);
1569     this.canvas.addEventListener("dblclick", mouseListener.mouseDoubleClicked);
1570     // Add pointermove and pointerup event listeners to window to capture pointer events out of the canvas 
1571     window.addEventListener("pointermove", mouseListener.windowPointerMoved);
1572     window.addEventListener("pointerup", mouseListener.windowPointerReleased);
1573   } else {
1574     this.canvas.addEventListener("touchstart", mouseListener.touchStarted);
1575     this.canvas.addEventListener("touchmove", mouseListener.touchMoved);
1576     this.canvas.addEventListener("touchend", mouseListener.touchEnded);
1577     this.canvas.addEventListener("mousedown", mouseListener.mousePressed);
1578     this.canvas.addEventListener("dblclick", mouseListener.mouseDoubleClicked);
1579     window.addEventListener("mousemove", mouseListener.windowMouseMoved);
1580     window.addEventListener("mouseup", mouseListener.windowMouseReleased);
1581   }
1582   this.canvas.addEventListener("contextmenu", mouseListener.contextMenuDisplayed);
1583   this.canvas.addEventListener("mousewheel", mouseListener.mouseWheelMoved);
1584   
1585   this.mouseListener = mouseListener;
1586 }
1587 
1588 /** 
1589  * @private 
1590  */
1591 PlanComponent.prototype.startLongTouchAnimation = function(x, y, character, animationPostTask) {
1592   this.touchOverlay.style.visibility = "visible";
1593   if (character == '⇪') {
1594     document.getElementById("plan-touch-overlay-timer-content").innerHTML = "<span style='font-weight: bold; font-family: sans-serif; font-size: 140%; line-height: 90%'>⇪</span>";
1595   } else {
1596     document.getElementById("plan-touch-overlay-timer-content").innerHTML = "<span style='font-weight: bold; font-family: sans-serif'>" + character + "</span>";
1597   }
1598   this.touchOverlay.style.left = (this.canvas.getBoundingClientRect().left + x - this.touchOverlay.clientWidth / 2) + "px";
1599   this.touchOverlay.style.top = (this.canvas.getBoundingClientRect().top + y - this.touchOverlay.clientHeight - 40) + "px";
1600   if (this.tooltip.style.visibility == "visible"
1601       && this.tooltip.getBoundingClientRect().top < this.canvas.getBoundingClientRect().top + y) {
1602     this.tooltip.style.marginTop = -(this.tooltip.clientHeight + 70) + "px";
1603   }
1604   for (var i = 0; i < this.touchOverlay.children.length; i++) {
1605     this.touchOverlay.children.item(i).classList.remove("indicator");
1606     this.touchOverlay.children.item(i).classList.add("animated");
1607   }
1608   if (animationPostTask !== undefined) {
1609     this.longTouchAnimationPostTask = setTimeout(animationPostTask, PlanComponent.LONG_TOUCH_DURATION_AFTER_DELAY);
1610   }
1611 }
1612 
1613 /** 
1614  * @private 
1615  */
1616 PlanComponent.prototype.startIndicatorAnimation = function(x, y, indicator) {
1617   if (indicator == "default" || indicator == "selection") {
1618     this.touchOverlay.style.visibility = "hidden";
1619   } else {
1620     this.touchOverlay.style.visibility = "visible";
1621     document.getElementById("plan-touch-overlay-timer-content").innerHTML = '<img src="' + ZIPTools.getScriptFolder() + 'resources/cursors/' + indicator + '32x32.png"/>';
1622     this.touchOverlay.style.left = (this.canvas.getBoundingClientRect().left + x - this.touchOverlay.clientWidth / 2) + "px";
1623     this.touchOverlay.style.top = (this.canvas.getBoundingClientRect().top + y - this.touchOverlay.clientHeight - 40) + "px";
1624     if (this.tooltip.style.visibility == "visible" 
1625         && this.tooltip.getBoundingClientRect().top < this.canvas.getBoundingClientRect().top + y) {
1626       this.tooltip.style.marginTop = -(this.tooltip.clientHeight + 70) + "px";
1627     }
1628     for (var i = 0; i < this.touchOverlay.children.length; i++) {
1629       this.touchOverlay.children.item(i).classList.remove("animated");
1630       this.touchOverlay.children.item(i).classList.add("indicator");
1631     }
1632   }
1633 }
1634 
1635 /** 
1636  * @private 
1637  */
1638 PlanComponent.prototype.stopLongTouchAnimation = function(x, y) {
1639   this.touchOverlay.style.visibility = "hidden";
1640   for (var i = 0; i < this.touchOverlay.children.length; i++) {
1641     this.touchOverlay.children.item(i).classList.remove("animated");
1642     this.touchOverlay.children.item(i).classList.remove("indicator");
1643   }
1644   if (this.longTouchAnimationPostTask !== undefined) {
1645     clearTimeout(this.longTouchAnimationPostTask);
1646     delete this.longTouchAnimationPostTask;
1647   }
1648 }
1649 
1650 /** 
1651  * @private 
1652  */
1653 PlanComponent.prototype.stopIndicatorAnimation = function() {
1654   this.touchOverlay.style.visibility = "hidden";
1655   for (var i = 0; i < this.touchOverlay.children.length; i++) {
1656     this.touchOverlay.children.item(i).classList.remove("animated");
1657     this.touchOverlay.children.item(i).classList.remove("indicator");
1658   }
1659 }
1660 
1661 /**
1662  * Adds focus listener to this component that calls back <code>controller</code>
1663  * escape method on focus lost event.
1664  * @param {PlanController} controller
1665  * @private
1666  */
1667 PlanComponent.prototype.addFocusListener = function(controller) {
1668   var plan = this;
1669   this.focusOutListener = function() {
1670       if (plan.pointerType === View.PointerType.TOUCH
1671           && plan.lastTouchEndX
1672           && plan.lastTouchEndY
1673           && controller.isModificationState()
1674           && (controller.getMode() === PlanController.Mode.WALL_CREATION
1675               || controller.getMode() === PlanController.Mode.ROOM_CREATION
1676               || controller.getMode() === PlanController.Mode.POLYLINE_CREATION
1677               || controller.getMode() === PlanController.Mode.DIMENSION_LINE_CREATION)) {
1678         // Emulate a mouse click at last touch location to validate last entered point
1679         controller.pressMouse(plan.convertXPixelToModel(plan.lastTouchEndX), 
1680             plan.convertYPixelToModel(plan.lastTouchEndY), 1, false, false, false, false, View.PointerType.TOUCH);
1681         controller.releaseMouse(plan.convertXPixelToModel(plan.lastTouchEndX), 
1682             plan.convertYPixelToModel(plan.lastTouchEndY));
1683       }
1684       plan.mouseListener.lastPointerLocation = null;
1685       plan.mouseListener.actionStartedInPlanComponent = false;
1686       controller.escape();
1687     };
1688   this.container.addEventListener("focusout", this.focusOutListener);
1689 }
1690 
1691 /**
1692  * Adds a listener to the controller to follow changes in base plan modification state.
1693  * @param {PlanController} controller
1694  * @private
1695  */
1696 PlanComponent.prototype.addControllerListener = function(controller) {
1697   var plan = this;
1698   controller.addPropertyChangeListener("BASE_PLAN_MODIFICATION_STATE", 
1699       function(ev) {
1700         var wallsDoorsOrWindowsModification = controller.isBasePlanModificationState();
1701         if (wallsDoorsOrWindowsModification) {
1702           if (controller.getMode() !== PlanController.Mode.WALL_CREATION) {
1703             var items = plan.draggedItemsFeedback != null ? plan.draggedItemsFeedback : plan.home.getSelectedItems();
1704             for (var i = 0; i < items.length; i++) {
1705               var item = items[i];
1706               if (!(item instanceof Wall) 
1707                   && !((item instanceof HomePieceOfFurniture) && item.isDoorOrWindow())) {
1708                 wallsDoorsOrWindowsModification = false;
1709               }
1710             }
1711           }
1712         }
1713         if (plan.wallsDoorsOrWindowsModification !== wallsDoorsOrWindowsModification) {
1714           plan.wallsDoorsOrWindowsModification = wallsDoorsOrWindowsModification;
1715           plan.repaint();
1716         }
1717       });
1718 }
1719 
1720 /**
1721  * Installs default keys bound to actions.
1722  * @private
1723  */
1724 PlanComponent.prototype.installDefaultKeyboardActions = function() {
1725   var plan = this;
1726   this.inputMap = {
1727       "pressed DELETE": "DELETE_SELECTION",
1728       "pressed BACK_SPACE": "DELETE_SELECTION",
1729       "pressed ESCAPE": "ESCAPE",
1730       "shift pressed ESCAPE": "ESCAPE",
1731       "pressed LEFT": "MOVE_SELECTION_LEFT",
1732       "shift pressed LEFT": "MOVE_SELECTION_FAST_LEFT",
1733       "pressed UP": "MOVE_SELECTION_UP",
1734       "shift pressed UP": "MOVE_SELECTION_FAST_UP",
1735       "pressed DOWN": "MOVE_SELECTION_DOWN",
1736       "shift pressed DOWN": "MOVE_SELECTION_FAST_DOWN",
1737       "pressed RIGHT": "MOVE_SELECTION_RIGHT",
1738       "shift pressed RIGHT": "MOVE_SELECTION_FAST_RIGHT",
1739       "pressed ENTER": "ACTIVATE_EDITIION",
1740       "shift pressed ENTER": "ACTIVATE_EDITIION"
1741   };
1742   if (OperatingSystem.isMacOSX()) {
1743     CoreTools.merge(this.inputMap, {
1744       "alt pressed ALT": "ACTIVATE_DUPLICATION",
1745       "released ALT": "DEACTIVATE_DUPLICATION",
1746       "shift alt pressed ALT": "ACTIVATE_DUPLICATION",
1747       "shift released ALT": "DEACTIVATE_DUPLICATION",
1748       "meta alt pressed ALT": "ACTIVATE_DUPLICATION",
1749       "meta released ALT": "DEACTIVATE_DUPLICATION",
1750       "shift meta alt pressed ALT": "ACTIVATE_DUPLICATION",
1751       "shift meta released ALT": "DEACTIVATE_DUPLICATION",
1752       "alt pressed ESCAPE": "ESCAPE",
1753       "alt pressed ENTER": "ACTIVATE_EDITIION"
1754     });
1755   }
1756   else {
1757     CoreTools.merge(this.inputMap, {
1758       "control pressed CONTROL": "ACTIVATE_DUPLICATION",
1759       "released CONTROL": "DEACTIVATE_DUPLICATION",
1760       "shift control pressed CONTROL": "ACTIVATE_DUPLICATION",
1761       "shift released CONTROL": "DEACTIVATE_DUPLICATION",
1762       "meta control pressed CONTROL": "ACTIVATE_DUPLICATION",
1763       "meta released CONTROL": "DEACTIVATE_DUPLICATION",
1764       "shift meta control pressed CONTROL": "ACTIVATE_DUPLICATION",
1765       "shift meta released CONTROL": "DEACTIVATE_DUPLICATION",
1766       "control pressed ESCAPE": "ESCAPE",
1767       "control pressed ENTER": "ACTIVATE_EDITIION"
1768     });
1769   }
1770   if (OperatingSystem.isWindows()) {
1771     CoreTools.merge(this.inputMap, {
1772       "alt pressed ALT": "TOGGLE_MAGNETISM_ON",
1773       "released ALT": "TOGGLE_MAGNETISM_OFF",
1774       "shift alt pressed ALT": "TOGGLE_MAGNETISM_ON",
1775       "shift released ALT": "TOGGLE_MAGNETISM_OFF",
1776       "control alt pressed ALT": "TOGGLE_MAGNETISM_ON",
1777       "control released ALT": "TOGGLE_MAGNETISM_OFF",
1778       "shift control alt pressed ALT": "TOGGLE_MAGNETISM_ON",
1779       "shift control released ALT": "TOGGLE_MAGNETISM_OFF",
1780       "alt pressed ESCAPE": "ESCAPE",
1781       "alt pressed ENTER": "ACTIVATE_EDITIION"
1782     });
1783   }
1784   else if (OperatingSystem.isMacOSX()) {
1785     CoreTools.merge(this.inputMap, {
1786       "meta pressed META": "TOGGLE_MAGNETISM_ON",
1787       "released META": "TOGGLE_MAGNETISM_OFF",
1788       "shift meta pressed META": "TOGGLE_MAGNETISM_ON",
1789       "shift released META": "TOGGLE_MAGNETISM_OFF",
1790       "alt meta pressed META": "TOGGLE_MAGNETISM_ON",
1791       "alt released META": "TOGGLE_MAGNETISM_OFF",
1792       "shift alt meta pressed META": "TOGGLE_MAGNETISM_ON",
1793       "shift alt released META": "TOGGLE_MAGNETISM_OFF",
1794       "meta pressed ESCAPE": "ESCAPE",
1795       "meta pressed ENTER": "ACTIVATE_EDITIION"
1796     });
1797   }
1798   else {
1799     CoreTools.merge(this.inputMap, {
1800       "shift alt pressed ALT": "TOGGLE_MAGNETISM_ON",
1801       "alt shift pressed SHIFT": "TOGGLE_MAGNETISM_ON",
1802       "alt released SHIFT": "TOGGLE_MAGNETISM_OFF",
1803       "shift released ALT": "TOGGLE_MAGNETISM_OFF",
1804       "control shift alt pressed ALT": "TOGGLE_MAGNETISM_ON",
1805       "control alt shift pressed SHIFT": "TOGGLE_MAGNETISM_ON",
1806       "control alt released SHIFT": "TOGGLE_MAGNETISM_OFF",
1807       "control shift released ALT": "TOGGLE_MAGNETISM_OFF",
1808       "alt shift pressed ESCAPE": "ESCAPE",
1809       "alt shift  pressed ENTER": "ACTIVATE_EDITIION",
1810       "control alt shift pressed ESCAPE": "ESCAPE",
1811       "control alt shift pressed ENTER": "ACTIVATE_EDITIION"
1812     });
1813   }
1814   CoreTools.merge(this.inputMap, {
1815     "shift pressed SHIFT": "ACTIVATE_ALIGNMENT",
1816     "released SHIFT": "DEACTIVATE_ALIGNMENT"
1817   });
1818   if (OperatingSystem.isWindows()) {
1819     CoreTools.merge(this.inputMap, {
1820       "control shift pressed SHIFT": "ACTIVATE_ALIGNMENT",
1821       "control released SHIFT": "DEACTIVATE_ALIGNMENT",
1822       "alt shift pressed SHIFT": "ACTIVATE_ALIGNMENT",
1823       "alt released SHIFT": "DEACTIVATE_ALIGNMENT"
1824     });
1825   }
1826   else if (OperatingSystem.isMacOSX()) {
1827     CoreTools.merge(this.inputMap, {
1828       "alt shift pressed SHIFT": "ACTIVATE_ALIGNMENT",
1829       "alt released SHIFT": "DEACTIVATE_ALIGNMENT",
1830       "meta shift pressed SHIFT": "ACTIVATE_ALIGNMENT",
1831       "meta released SHIFT": "DEACTIVATE_ALIGNMENT"
1832     });
1833   }
1834   else {
1835     CoreTools.merge(this.inputMap, {
1836       "control shift pressed SHIFT": "ACTIVATE_ALIGNMENT",
1837       "control released SHIFT": "DEACTIVATE_ALIGNMENT",
1838       "shift released ALT": "ACTIVATE_ALIGNMENT",
1839       "control shift released ALT": "ACTIVATE_ALIGNMENT"
1840     });
1841   }
1842   this.keyDownListener = function(ev) { 
1843       return plan.callAction(ev, "keydown"); 
1844     };
1845   this.container.addEventListener("keydown", this.keyDownListener, false);
1846   this.keyUpListener = function(ev) { 
1847       return plan.callAction(ev, "keyup"); 
1848     };
1849   this.container.addEventListener("keyup", this.keyUpListener, false);
1850 }
1851 
1852 /**
1853  * Runs the action bound to the key event in parameter.
1854  * @private
1855  */
1856 PlanComponent.prototype.callAction = function(ev, keyType) {
1857   var keyStroke = KeyStroke.getKeyStrokeForEvent(ev, keyType);
1858   if (keyStroke !== undefined) {
1859     var actionKey = this.inputMap[keyStroke];
1860     if (actionKey !== undefined) {
1861       var action = this.actionMap[actionKey];
1862       if (action !== undefined) {
1863         action.actionPerformed(ev);
1864       }
1865       ev.stopPropagation();
1866     }
1867   }
1868 }
1869 
1870 /**
1871  * Installs keys bound to actions during edition.
1872  * @private
1873  */
1874 PlanComponent.prototype.installEditionKeyboardActions = function() {
1875   this.inputMap = {
1876       "ESCAPE": "ESCAPE",
1877       "shift ESCAPE": "ESCAPE",
1878       "ENTER": "DEACTIVATE_EDITIION",
1879       "shift ENTER": "DEACTIVATE_EDITIION"
1880   };
1881   if (OperatingSystem.isMacOSX()) {
1882     CoreTools.merge(this.inputMap, {
1883       "alt ESCAPE": "ESCAPE",
1884       "alt ENTER": "DEACTIVATE_EDITIION",
1885       "alt shift ENTER": "DEACTIVATE_EDITIION",
1886       "alt pressed ALT": "ACTIVATE_DUPLICATION",
1887       "released ALT": "DEACTIVATE_DUPLICATION",
1888       "shift alt pressed ALT": "ACTIVATE_DUPLICATION",
1889       "shift released ALT": "DEACTIVATE_DUPLICATION"
1890     });
1891   }
1892   else {
1893     CoreTools.merge(this.inputMap, {
1894       "control ESCAPE": "ESCAPE",
1895       "control ENTER": "DEACTIVATE_EDITIION",
1896       "control shift ENTER": "DEACTIVATE_EDITIION",
1897       "control pressed CONTROL": "ACTIVATE_DUPLICATION",
1898       "released CONTROL": "DEACTIVATE_DUPLICATION",
1899       "shift control pressed CONTROL": "ACTIVATE_DUPLICATION",
1900       "shift released CONTROL": "DEACTIVATE_DUPLICATION"
1901     });
1902   }
1903 }
1904 
1905 /**
1906  * Creates actions that calls back <code>controller</code> methods.
1907  * @param {PlanController} controller
1908  * @private
1909  */
1910 PlanComponent.prototype.createActions = function(controller) {
1911   var plan = this;
1912   
1913   function MoveSelectionAction(dx, dy) {
1914     this.dx = dx;
1915     this.dy = dy;
1916   }
1917   MoveSelectionAction.prototype.actionPerformed = function(ev) {
1918     controller.moveSelection(this.dx / plan.getScale(), this.dy / plan.getScale());
1919   };
1920     
1921   function ToggleMagnetismAction(toggle) {
1922     this.toggle = toggle;
1923   }
1924   ToggleMagnetismAction.prototype.actionPerformed = function(ev) {
1925     controller.toggleMagnetism(this.toggle);
1926   };
1927     
1928   function SetAlignmentActivatedAction(alignmentActivated) {
1929     this.alignmentActivated = alignmentActivated;
1930   }
1931   SetAlignmentActivatedAction.prototype.actionPerformed = function(ev) {
1932     controller.setAlignmentActivated(this.alignmentActivated);
1933   };
1934     
1935   function SetDuplicationActivatedAction(duplicationActivated) {
1936     this.duplicationActivated = duplicationActivated;
1937   }
1938   SetDuplicationActivatedAction.prototype.actionPerformed = function(ev) {
1939     controller.setDuplicationActivated(this.duplicationActivated);
1940   };
1941     
1942   function SetEditionActivatedAction(editionActivated) {
1943     this.editionActivated = editionActivated;
1944   }
1945   SetEditionActivatedAction.prototype.actionPerformed = function(ev) {
1946     controller.setEditionActivated(this.editionActivated);
1947   };
1948     
1949   this.actionMap = {
1950       "DELETE_SELECTION": { 
1951           actionPerformed: function() { 
1952               controller.deleteSelection(); 
1953             } 
1954           },
1955       "ESCAPE": { 
1956            actionPerformed: function() { 
1957                controller.escape(); 
1958              } 
1959           },
1960       "MOVE_SELECTION_LEFT": new MoveSelectionAction(-1, 0),
1961       "MOVE_SELECTION_FAST_LEFT": new MoveSelectionAction(-10, 0),
1962       "MOVE_SELECTION_UP": new MoveSelectionAction(0, -1),
1963       "MOVE_SELECTION_FAST_UP": new MoveSelectionAction(0, -10),
1964       "MOVE_SELECTION_DOWN": new MoveSelectionAction(0, 1),
1965       "MOVE_SELECTION_FAST_DOWN": new MoveSelectionAction(0, 10),
1966       "MOVE_SELECTION_RIGHT": new MoveSelectionAction(1, 0),
1967       "MOVE_SELECTION_FAST_RIGHT": new MoveSelectionAction(10, 0),
1968       "TOGGLE_MAGNETISM_ON": new ToggleMagnetismAction(true),
1969       "TOGGLE_MAGNETISM_OFF": new ToggleMagnetismAction(false),
1970       "ACTIVATE_ALIGNMENT": new SetAlignmentActivatedAction(true),
1971       "DEACTIVATE_ALIGNMENT": new SetAlignmentActivatedAction(false),
1972       "ACTIVATE_DUPLICATION": new SetDuplicationActivatedAction(true),
1973       "DEACTIVATE_DUPLICATION": new SetDuplicationActivatedAction(false),
1974       "ACTIVATE_EDITIION": new SetEditionActivatedAction(true),
1975       "DEACTIVATE_EDITIION": new SetEditionActivatedAction(false)
1976   };
1977 }
1978 
1979 PlanComponent.createCustomCursor = function(name, defaultCursor) {
1980    if (OperatingSystem.isInternetExplorer()) {
1981      return defaultCursor;
1982    } else {
1983      return 'url("' + ZIPTools.getScriptFolder() + '/resources/cursors/' 
1984                  + name + '16x16' + (OperatingSystem.isMacOSX() ? '-macosx' : '') + '.png") 8 8, ' + defaultCursor;
1985    }
1986 }
1987 
1988 /**
1989  * Returns the preferred size of this component in actual screen pixels size.
1990  * @return {java.awt.Dimension}
1991  */
1992 PlanComponent.prototype.getPreferredSize = function() {
1993   var insets = this.getInsets();
1994   var planBounds = this.getPlanBounds();
1995   return {width:  this.convertLengthToPixel(planBounds.getWidth() + PlanComponent.MARGIN * 2) + insets.left + insets.right,
1996           height: this.convertLengthToPixel(planBounds.getHeight() + PlanComponent.MARGIN * 2) + insets.top + insets.bottom};
1997 }
1998 
1999 /** 
2000  * @private 
2001  */
2002 PlanComponent.prototype.getInsets = function() {
2003   return { top: 0, bottom: 0, left: 0, right: 0 };
2004 }
2005 
2006 /** 
2007  * @private 
2008  */
2009 PlanComponent.prototype.getWidth = function() {
2010   return this.view.clientWidth;
2011 }
2012 
2013 /** 
2014  * @private 
2015  */
2016 PlanComponent.prototype.getHeight = function() {
2017   return this.view.clientHeight;
2018 }
2019 
2020 /**
2021  * Returns the bounds of the plan displayed by this component.
2022  * @return {java.awt.geom.Rectangle2D}
2023  * @private
2024  */
2025 PlanComponent.prototype.getPlanBounds = function() {
2026   var plan = this;
2027   if (!this.planBoundsCacheValid) {
2028     if (this.planBoundsCache == null) {
2029       this.planBoundsCache = new java.awt.geom.Rectangle2D.Float(0, 0, 1000, 1000);
2030     }
2031     if (this.backgroundImageCache != null) {
2032       var backgroundImage = this.home.getBackgroundImage();
2033       if (backgroundImage != null) {
2034         this.planBoundsCache.add(-backgroundImage.getXOrigin(), -backgroundImage.getYOrigin());
2035         this.planBoundsCache.add(this.backgroundImageCache.width * backgroundImage.getScale() - backgroundImage.getXOrigin(), 
2036             this.backgroundImageCache.height * backgroundImage.getScale() - backgroundImage.getYOrigin());
2037       }
2038       this.home.getLevels().forEach(function(level) {
2039           var levelBackgroundImage = level.getBackgroundImage();
2040           if (levelBackgroundImage != null) {
2041             plan.planBoundsCache.add(-levelBackgroundImage.getXOrigin(), -levelBackgroundImage.getYOrigin());
2042             plan.planBoundsCache.add(plan.backgroundImageCache.width * levelBackgroundImage.getScale() - levelBackgroundImage.getXOrigin(), 
2043                 plan.backgroundImageCache.height * levelBackgroundImage.getScale() - levelBackgroundImage.getYOrigin());
2044           }
2045         });
2046     }
2047     var g = this.getGraphics();
2048     if (g != null) {
2049       this.setRenderingHints(g);
2050     }
2051     var homeItemsBounds = this.getItemsBounds(g, this.getPaintedItems());
2052     if (homeItemsBounds != null) {
2053       this.planBoundsCache.add(homeItemsBounds);
2054     }
2055     this.home.getObserverCamera().getPoints().forEach(function(point) { 
2056         return plan.planBoundsCache.add(point[0], point[1]); 
2057       });
2058     this.planBoundsCacheValid = true;
2059   }
2060   return this.planBoundsCache;
2061 }
2062 
2063 /**
2064  * Returns the collection of walls, furniture, rooms and dimension lines of the home
2065  * painted by this component wherever the level they belong to is selected or not.
2066  * @return {Object[]}
2067  */
2068 PlanComponent.prototype.getPaintedItems = function() {
2069   return this.home.getSelectableViewableItems();
2070 }
2071 
2072 /**
2073  * Returns the bounds of the given collection of <code>items</code>.
2074  * @param {Graphics2D} g
2075  * @param {Bound[]} items
2076  * @return {java.awt.geom.Rectangle2D}
2077  * @private
2078  */
2079 PlanComponent.prototype.getItemsBounds = function(g, items) {
2080   var itemsBounds = null;
2081   for (var i = 0; i < items.length; i++) {
2082     var item = items[i];
2083     var itemBounds = this.getItemBounds(g, item);
2084     if (itemsBounds == null) {
2085       itemsBounds = itemBounds;
2086     } else {
2087       itemsBounds.add(itemBounds);
2088     }
2089   }
2090   return itemsBounds;
2091 }
2092 
2093 /**
2094  * Returns the bounds of the given <code>item</code>.
2095  * @param {Graphics2D} g
2096  * @param {Object} item
2097  * @return {java.awt.geom.Rectangle2D}
2098  */
2099 PlanComponent.prototype.getItemBounds = function(g, item) {
2100   var plan = this;
2101   var points = item.getPoints();
2102   var itemBounds = new java.awt.geom.Rectangle2D.Float(points[0][0], points[0][1], 0, 0);
2103   for (var i = 1; i < points.length; i++) {
2104     itemBounds.add(points[i][0], points[i][1]);
2105   }
2106   var componentFont;
2107   if (g != null) {
2108     componentFont = g.getFont();
2109   } else {
2110     componentFont = this.getFont();
2111   }
2112   if (item instanceof Room) {
2113     var room = item;
2114     var xRoomCenter = room.getXCenter();
2115     var yRoomCenter = room.getYCenter();
2116     var roomName = room.getName();
2117     if (roomName != null && roomName.length > 0) {
2118       this.addTextBounds(room.constructor, 
2119           roomName, room.getNameStyle(), 
2120           xRoomCenter + room.getNameXOffset(), 
2121           yRoomCenter + room.getNameYOffset(), room.getNameAngle(), itemBounds);
2122     }
2123     if (room.isAreaVisible()) {
2124       var area = room.getArea();
2125       if (area > 0.01) {
2126         var areaText = this.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);
2127         this.addTextBounds(room.constructor, 
2128             areaText, room.getAreaStyle(), 
2129             xRoomCenter + room.getAreaXOffset(), 
2130             yRoomCenter + room.getAreaYOffset(), room.getAreaAngle(), itemBounds);
2131       }
2132     }
2133   } else if (item instanceof Polyline) {
2134     var polyline = item;
2135     return ShapeTools.getPolylineShape(polyline.getPoints(), 
2136         polyline.getJoinStyle() === Polyline.JoinStyle.CURVED, polyline.isClosedPath()).getBounds2D();
2137   } else if (item instanceof HomePieceOfFurniture) {
2138     if (item != null && item instanceof HomeDoorOrWindow) {
2139       var doorOrWindow_1 = item;
2140       doorOrWindow_1.getSashes().forEach(function(sash) {
2141         itemBounds.add(plan.getDoorOrWindowSashShape(doorOrWindow_1, sash).getBounds2D());
2142       });
2143     } else if (item instanceof HomeFurnitureGroup) {
2144       itemBounds.add(this.getItemsBounds(g, item.getFurniture()));
2145     }
2146     var piece = item;
2147     var pieceName = piece.getName();
2148     if (piece.isVisible() 
2149         && piece.isNameVisible() 
2150         && pieceName.length > 0) {
2151       this.addTextBounds(piece.constructor, 
2152           pieceName, piece.getNameStyle(), 
2153           piece.getX() + piece.getNameXOffset(), 
2154           piece.getY() + piece.getNameYOffset(), piece.getNameAngle(), itemBounds);
2155     }
2156   } else if (item instanceof DimensionLine) {
2157     var dimensionLine = item;
2158     var dimensionLineLength = dimensionLine.getLength();
2159     var lengthText = this.preferences.getLengthUnit().getFormat().format(dimensionLineLength);
2160     var lengthStyle = dimensionLine.getLengthStyle();
2161     if (lengthStyle == null) {
2162       lengthStyle = this.preferences.getDefaultTextStyle(dimensionLine.constructor);
2163     }
2164     var transform = java.awt.geom.AffineTransform.getTranslateInstance(
2165         dimensionLine.getXStart(), dimensionLine.getYStart());
2166     var angle = dimensionLine.isElevationDimensionLine()
2167         ? (dimensionLine.getPitch() + 2 * Math.PI) % (2 * Math.PI)
2168         : Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(), dimensionLine.getXEnd() - dimensionLine.getXStart());
2169     if (dimensionLine.getElevationStart() == dimensionLine.getElevationEnd()) {
2170       var lengthFontMetrics = this.getFontMetrics(componentFont, lengthStyle);
2171       var lengthTextBounds = lengthFontMetrics.getStringBounds(lengthText);
2172       transform.rotate(angle);
2173       transform.translate(0, dimensionLine.getOffset());
2174       transform.translate((dimensionLineLength - lengthTextBounds.getWidth()) / 2, 
2175           dimensionLine.getOffset() <= 0 
2176               ? -lengthFontMetrics.getDescent() - 1 
2177               : lengthFontMetrics.getAscent() + 1);
2178       var lengthTextBoundsPath = new java.awt.geom.GeneralPath(lengthTextBounds);
2179       for (var it = lengthTextBoundsPath.getPathIterator(transform); !it.isDone(); it.next()) {
2180         var pathPoint = [0, 0];
2181         if (it.currentSegment(pathPoint) !== java.awt.geom.PathIterator.SEG_CLOSE) {
2182           itemBounds.add(pathPoint[0], pathPoint[1]);
2183         }
2184       }
2185     }
2186     transform.setToTranslation(dimensionLine.getXStart(), dimensionLine.getYStart());
2187     transform.rotate(angle);
2188     transform.translate(0, dimensionLine.getOffset());
2189     for (var it = PlanComponent.DIMENSION_LINE_MARK_END.getPathIterator(transform); !it.isDone(); it.next()) {
2190       var pathPoint = [0, 0];
2191       if (it.currentSegment(pathPoint) !== java.awt.geom.PathIterator.SEG_CLOSE) {
2192         itemBounds.add(pathPoint[0], pathPoint[1]);
2193       }
2194     }
2195     transform.translate(dimensionLineLength, 0);
2196     for (var it = PlanComponent.DIMENSION_LINE_MARK_END.getPathIterator(transform); !it.isDone(); it.next()) {
2197       var pathPoint = [0, 0];
2198       if (it.currentSegment(pathPoint) !== java.awt.geom.PathIterator.SEG_CLOSE) {
2199         itemBounds.add(pathPoint[0], pathPoint[1]);
2200       }
2201     }
2202   } else if (item instanceof Label) {
2203     var label = item;
2204     this.addTextBounds(label.constructor, 
2205         label.getText(), label.getStyle(), label.getX(), label.getY(), label.getAngle(), itemBounds);
2206   } else if (item instanceof Compass) {
2207     var compass = item;
2208     var transform = java.awt.geom.AffineTransform.getTranslateInstance(compass.getX(), compass.getY());
2209     transform.scale(compass.getDiameter(), compass.getDiameter());
2210     transform.rotate(compass.getNorthDirection());
2211     return PlanComponent.COMPASS.createTransformedShape(transform).getBounds2D();
2212   }
2213   return itemBounds;
2214 }
2215 
2216 /**
2217  * Add <code>text</code> bounds to the given rectangle <code>bounds</code>.
2218  * @param {Object} selectableClass
2219  * @param {string} text
2220  * @param {TextStyle} style
2221  * @param {number} x
2222  * @param {number} y
2223  * @param {number} angle
2224  * @param {java.awt.geom.Rectangle2D} bounds
2225  * @private
2226  */
2227 PlanComponent.prototype.addTextBounds = function(selectableClass, text, style, x, y, angle, bounds) {
2228   if (style == null) {
2229     style = this.preferences.getDefaultTextStyle(selectableClass);
2230   }
2231   this.getTextBounds(text, style, x, y, angle).forEach(function(points) { 
2232       return bounds.add(points[0], points[1]); 
2233     });
2234 }
2235 
2236 /**
2237  * Returns the coordinates of the bounding rectangle of the <code>text</code> centered at
2238  * the point (<code>x</code>,<code>y</code>).
2239  * @param {string} text
2240  * @param {TextStyle} style
2241  * @param {number} x
2242  * @param {number} y
2243  * @param {number} angle
2244  * @return {Array}
2245  */
2246 PlanComponent.prototype.getTextBounds = function(text, style, x, y, angle) {
2247   var fontMetrics = this.getFontMetrics(this.getFont(), style);
2248   var textBounds = null;
2249   var lines = text.replace(/\n*$/, "").split("\n");
2250   var g = this.getGraphics();
2251   if (g != null) {
2252     this.setRenderingHints(g);
2253   }
2254   for (var i = 0; i < lines.length; i++) {
2255     var lineBounds = fontMetrics.getStringBounds(lines[i]);
2256     if (textBounds == null || textBounds.getWidth() < lineBounds.getWidth()) {
2257       textBounds = lineBounds;
2258     }
2259   }
2260   var textWidth = textBounds.getWidth();
2261   var shiftX;
2262   if (style.getAlignment() === TextStyle.Alignment.LEFT) {
2263     shiftX = 0;
2264   }
2265   else if (style.getAlignment() === TextStyle.Alignment.RIGHT) {
2266     shiftX = -textWidth;
2267   }
2268   else {
2269     shiftX = -textWidth / 2;
2270   }
2271   if (angle === 0) {
2272     var minY = (y + textBounds.getY());
2273     var maxY = (minY + textBounds.getHeight());
2274     minY -= (textBounds.getHeight() * (lines.length - 1));
2275     return [
2276         [x + shiftX, minY], 
2277         [x + shiftX + textWidth, minY], 
2278         [x + shiftX + textWidth, maxY], 
2279         [x + shiftX, maxY]];
2280   } else {
2281     textBounds.add(textBounds.getX(), textBounds.getY() - textBounds.getHeight() * (lines.length - 1));
2282     var transform = new java.awt.geom.AffineTransform();
2283     transform.translate(x, y);
2284     transform.rotate(angle);
2285     transform.translate(shiftX, 0);
2286     var textBoundsPath = new java.awt.geom.GeneralPath(textBounds);
2287     var textPoints = [];
2288     for (var it = textBoundsPath.getPathIterator(transform); !it.isDone(); it.next()) {
2289       var pathPoint = [0, 0];
2290       if (it.currentSegment(pathPoint) !== java.awt.geom.PathIterator.SEG_CLOSE) {
2291         textPoints.push(pathPoint);
2292       }
2293     }
2294     return textPoints.slice(0);
2295   }
2296 }
2297 
2298 /**
2299  * Returns the HTML font matching a given text style.
2300  * @param {string} [defaultFont]
2301  * @param {TextStyle} [textStyle]
2302  * @return {string}
2303  */
2304 PlanComponent.prototype.getFont = function(defaultFont, textStyle) {
2305   if (defaultFont == null && textStyle == null) {
2306     return this.font;
2307   }
2308   if (this.fonts == null) {
2309     this.fonts = {};
2310   }
2311   var font = CoreTools.getFromMap(this.fonts, textStyle);
2312   if (font == null) {
2313     var fontStyle = 'normal';
2314     var fontWeight = 'normal';
2315     if (textStyle.isBold()) {
2316       fontWeight = 'bold';
2317     }
2318     if (textStyle.isItalic()) {
2319       fontStyle = 'italic';
2320     }
2321     if (defaultFont == null 
2322         || this.preferences.getDefaultFontName() != null 
2323         || textStyle.getFontName() != null) {
2324       var fontName = textStyle.getFontName();
2325       if (fontName == null) {
2326         fontName = this.preferences.getDefaultFontName();
2327       }
2328       if (fontName == null) {
2329         fontName = new Font(this.font).family;
2330       }
2331       defaultFont = new Font({ 
2332           style: fontStyle, 
2333           weight: fontWeight, 
2334           size: "10px", 
2335           family: fontName }).toString();
2336     }
2337     font = new Font({ 
2338         style: fontStyle, 
2339         weight: fontWeight, 
2340         size: textStyle.getFontSize() + "px", 
2341         family: new Font(defaultFont).family }).toString();
2342     CoreTools.putToMap(this.fonts, textStyle, font);
2343   }
2344   return font;
2345 }
2346 
2347 /** 
2348  * Sets the default font used to paint text in this component.
2349  * @param {string} font a HTML font
2350  * @private 
2351  */
2352 PlanComponent.prototype.setFont = function(font) {
2353   this.font = font;
2354 }
2355 
2356 /**
2357  * Returns the font metrics matching a given text style.
2358  * @param {string} defaultFont
2359  * @param {TextStyle} textStyle
2360  * @return {FontMetrics}
2361  */
2362 PlanComponent.prototype.getFontMetrics = function(defaultFont, textStyle) {
2363   if (textStyle == null) {
2364     return new FontMetrics(defaultFont);
2365   }
2366   if (this.fontsMetrics == null) {
2367     this.fontsMetrics = {};
2368   }
2369   var fontMetrics = CoreTools.getFromMap(this.fontsMetrics, textStyle);
2370   if (fontMetrics == null) {
2371     fontMetrics = this.getFontMetrics(this.getFont(defaultFont, textStyle));
2372     CoreTools.putToMap(this.fontsMetrics, textStyle, fontMetrics);
2373   }
2374   return fontMetrics;
2375 }
2376 
2377 /**
2378  * Sets whether plan's background should be painted or not.
2379  * Background may include grid and an image.
2380  * @param {boolean} backgroundPainted
2381  */
2382 PlanComponent.prototype.setBackgroundPainted = function(backgroundPainted) {
2383   if (this.backgroundPainted !== backgroundPainted) {
2384     this.backgroundPainted = backgroundPainted;
2385     this.repaint();
2386   }
2387 }
2388 
2389 /**
2390  * Returns <code>true</code> if plan's background should be painted.
2391  * @return {boolean}
2392  */
2393 PlanComponent.prototype.isBackgroundPainted = function() {
2394   return this.backgroundPainted;
2395 }
2396 
2397 /**
2398  * Sets whether the outline of home selected items should be painted or not.
2399  * @param {boolean} selectedItemsOutlinePainted
2400  */
2401 PlanComponent.prototype.setSelectedItemsOutlinePainted = function(selectedItemsOutlinePainted) {
2402   if (this.selectedItemsOutlinePainted !== selectedItemsOutlinePainted) {
2403     this.selectedItemsOutlinePainted = selectedItemsOutlinePainted;
2404     this.repaint();
2405   }
2406 }
2407 
2408 /**
2409  * Returns <code>true</code> if the outline of home selected items should be painted.
2410  * @return {boolean}
2411  */
2412 PlanComponent.prototype.isSelectedItemsOutlinePainted = function() {
2413   return this.selectedItemsOutlinePainted;
2414 }
2415 
2416 /**
2417  * Repaints this component elements when possible.
2418  */
2419 PlanComponent.prototype.repaint = function() {
2420   var plan = this;
2421   if (!this.canvasNeededRepaint) {
2422     this.canvasNeededRepaint = true;
2423     requestAnimationFrame(function() {
2424         if (plan.canvasNeededRepaint) {
2425           plan.canvasNeededRepaint = false;
2426           plan.paintComponent(plan.getGraphics());
2427         }
2428       });
2429   }
2430 }
2431 
2432 /**
2433  * Returns a <code>Graphics2D</code> object.
2434  * @return {Graphics2D}
2435  */
2436 PlanComponent.prototype.getGraphics = function() {
2437   if (!this.graphics) {
2438     this.graphics = new Graphics2D(this.canvas);
2439   }
2440   return this.graphics;
2441 }
2442 
2443 /**
2444  * Paints this component.
2445  * @param {Graphics2D} g
2446  */
2447 PlanComponent.prototype.paintComponent = function(g2D) {
2448   g2D.setTransform(new java.awt.geom.AffineTransform());
2449   g2D.clear();
2450   if (this.backgroundPainted) {
2451     this.paintBackground(g2D, this.getBackgroundColor(PlanComponent.PaintMode.PAINT));
2452   }
2453   var insets = this.getInsets();
2454   // g2D.clipRect(0, 0, this.getWidth(), this.getHeight());
2455   var planBounds = this.getPlanBounds();
2456   var planScale = this.getScale() * this.resolutionScale; 
2457   if (this.isScrolled()) {
2458     g2D.translate(-this.scrollPane.scrollLeft * this.resolutionScale, -this.scrollPane.scrollTop * this.resolutionScale);
2459   } 
2460   g2D.translate(insets.left * this.resolutionScale + (PlanComponent.MARGIN - planBounds.getMinX()) * planScale,
2461       insets.top * this.resolutionScale + (PlanComponent.MARGIN - planBounds.getMinY()) * planScale);
2462   g2D.scale(planScale, planScale);
2463   this.setRenderingHints(g2D);
2464   // For debugging only
2465   if (this.drawPlanBounds) {
2466     g2D.setColor("#FF0000");
2467     g2D.draw(planBounds);
2468   }
2469   this.paintContent(g2D, this.home.getSelectedLevel(), this.getScale(), PlanComponent.PaintMode.PAINT);
2470   g2D.dispose();
2471 }
2472 
2473 /**
2474  * Returns the print preferred scale of the plan drawn in this component
2475  * to make it fill <code>pageFormat</code> imageable size.
2476  * @param {Graphics2D} g
2477  * @param {java.awt.print.PageFormat} pageFormat
2478  * @return {number}
2479  */
2480 PlanComponent.prototype.getPrintPreferredScale = function(g, pageFormat) {
2481   return 1;
2482 }
2483 
2484 /**
2485  * Returns the stroke width used to paint an item of the given class.
2486  * @param {Object} itemClass
2487  * @param {PlanComponent.PaintMode} paintMode
2488  * @return {number}
2489  * @private
2490  */
2491 PlanComponent.prototype.getStrokeWidth = function(itemClass, paintMode) {
2492   var strokeWidth;
2493   if (Wall === itemClass || Room === itemClass) {
2494     strokeWidth = PlanComponent.WALL_STROKE_WIDTH;
2495   } else {
2496     strokeWidth = PlanComponent.BORDER_STROKE_WIDTH;
2497   }
2498   if (paintMode === PlanComponent.PaintMode.PRINT) {
2499     strokeWidth *= 0.5;
2500   }
2501   return strokeWidth;
2502 }
2503 
2504 /**
2505  * Returns an image of selected items in plan for transfer purpose.
2506  * @param {TransferableView.DataType} dataType
2507  * @return {Object}
2508  */
2509 PlanComponent.prototype.createTransferData = function(dataType) {
2510   if (dataType === TransferableView.DataType.PLAN_IMAGE) {
2511     return this.getClipboardImage();
2512   } else {
2513     return null;
2514   }
2515 }
2516 
2517 /**
2518  * Returns an image of the selected items displayed by this component
2519  * (camera excepted) with no outline at scale 1/1 (1 pixel = 1cm).
2520  * @return {HTMLImageElement}
2521  */
2522 PlanComponent.prototype.getClipboardImage = function() {
2523   throw new UnsupportedOperationException("Not implemented");
2524 }
2525 
2526 /**
2527  * Returns <code>true</code> if the given format is SVG.
2528  * @param {ExportableView.FormatType} formatType
2529  * @return {boolean}
2530  */
2531 PlanComponent.prototype.isFormatTypeSupported = function(formatType) {
2532   return false;
2533 }
2534 
2535 /**
2536  * Writes this plan in the given output stream at SVG (Scalable Vector Graphics) format if this is the requested format.
2537  * @param {java.io.OutputStream} out
2538  * @param {ExportableView.FormatType} formatType
2539  * @param {Object} settings
2540  */
2541 PlanComponent.prototype.exportData = function(out, formatType, settings) {
2542   throw new UnsupportedOperationException("Unsupported format " + formatType);
2543 }
2544 
2545 /**
2546  * Sets rendering hints used to paint plan.
2547  * @param {Graphics2D} g2D
2548  * @private
2549  */
2550 PlanComponent.prototype.setRenderingHints = function(g2D) {
2551   // TODO
2552 }
2553 
2554 /**
2555  * Fills the background.
2556  * @param {Graphics2D} g2D
2557  * @param {string} backgroundColor
2558  * @private
2559  */
2560 PlanComponent.prototype.paintBackground = function(g2D, backgroundColor) {
2561   if (this.isOpaque()) {
2562     g2D.setColor(backgroundColor);
2563     g2D.fillRect(0, 0, this.canvas.width, this.canvas.height);
2564   }
2565 }
2566 
2567 /** 
2568  * @private 
2569  */
2570 PlanComponent.prototype.isOpaque = function() {
2571   return this.opaque === true;
2572 }
2573 
2574 /** 
2575  * @private 
2576  */
2577 PlanComponent.prototype.setOpaque = function(opaque) {
2578   this.opaque = opaque;
2579 }
2580 
2581 /**
2582  * Paints background image and returns <code>true</code> if an image is painted.
2583  * @param {Graphics2D} g2D
2584  * @param {Level} level
2585  * @param {PlanComponent.PaintMode} paintMode
2586  * @return {boolean}
2587  * @private
2588  */
2589 PlanComponent.prototype.paintBackgroundImage = function(g2D, level, paintMode) {
2590   var selectedLevel = this.home.getSelectedLevel();
2591   var backgroundImageLevel = null;
2592   if (level != null) {
2593     var levels = this.home.getLevels();
2594     for (var i = levels.length - 1; i >= 0; i--) {
2595       var homeLevel = levels[i];
2596       if (homeLevel.getElevation() === level.getElevation() 
2597           && homeLevel.getElevationIndex() <= level.getElevationIndex() 
2598           && homeLevel.isViewable() 
2599           && homeLevel.getBackgroundImage() != null 
2600           && homeLevel.getBackgroundImage().isVisible()) {
2601         backgroundImageLevel = homeLevel;
2602         break;
2603       }
2604     }
2605   }
2606   var backgroundImage = backgroundImageLevel == null 
2607       ? this.home.getBackgroundImage() 
2608       : backgroundImageLevel.getBackgroundImage();
2609   if (backgroundImage != null && backgroundImage.isVisible()) {
2610     var previousTransform = g2D.getTransform();
2611     g2D.translate(-backgroundImage.getXOrigin(), -backgroundImage.getYOrigin());
2612     var backgroundImageScale = backgroundImage.getScale();
2613     g2D.scale(backgroundImageScale, backgroundImageScale);
2614     var oldAlpha = this.setTransparency(g2D, 0.7);
2615     g2D.drawImage(this.backgroundImageCache != null 
2616         ? this.backgroundImageCache 
2617         : this.readBackgroundImage(backgroundImage.getImage()), 0, 0);
2618     g2D.setAlpha(oldAlpha);
2619     g2D.setTransform(previousTransform);
2620     return true;
2621   }
2622   return false;
2623 }
2624 
2625 /**
2626  * Returns the foreground color used to draw content.
2627  * @param {PlanComponent.PaintMode} mode
2628  * @return {string}
2629  */
2630 PlanComponent.prototype.getForegroundColor = function(mode) {
2631   if (mode === PlanComponent.PaintMode.PAINT) {
2632     return this.getForeground();
2633   }
2634   else {
2635     return "#000000";
2636   }
2637 }
2638 
2639 /** 
2640  * @private 
2641  */
2642 PlanComponent.prototype.getForeground = function() {
2643   if (this.foreground == null) {
2644     this.foreground = ColorTools.styleToHexadecimalString(this.canvas.style.color);
2645   }
2646   return this.foreground;
2647 }
2648 
2649 /**
2650  * Returns the background color used to draw content.
2651  * @param {PlanComponent.PaintMode} mode
2652  * @return {string}
2653  */
2654 PlanComponent.prototype.getBackgroundColor = function(mode) {
2655   if (mode === PlanComponent.PaintMode.PAINT) {
2656     return this.getBackground();
2657   }
2658   else {
2659     return "#FFFFFF";
2660   }
2661 }
2662 
2663 /** 
2664  * @private 
2665  */
2666 PlanComponent.prototype.getBackground = function() {
2667   if (this.background == null) {
2668     this.background = ColorTools.styleToHexadecimalString(this.canvas.style.backgroundColor);
2669   }
2670   return this.background;
2671 }
2672 
2673 /**
2674  * Returns the image contained in <code>imageContent</code> or an empty image if reading failed.
2675  * @param {Content} imageContent
2676  * @return {HTMLImageElement}
2677  * @private
2678  */
2679 PlanComponent.prototype.readBackgroundImage = function(imageContent) {
2680   var plan = this;
2681   if (this.backgroundImageCache != PlanComponent.WAIT_TEXTURE_IMAGE 
2682       && this.backgroundImageCache != PlanComponent.ERROR_TEXTURE_IMAGE) {
2683     this.backgroundImageCache = PlanComponent.WAIT_TEXTURE_IMAGE;
2684     TextureManager.getInstance().loadTexture(imageContent, {
2685       textureUpdated: function(texture) {
2686         plan.backgroundImageCache = texture;
2687         plan.repaint();
2688       },
2689       textureError: function() {
2690         plan.backgroundImageCache = PlanComponent.ERROR_TEXTURE_IMAGE;
2691         plan.repaint();
2692       }
2693     });
2694   }
2695   return this.backgroundImageCache;
2696 }
2697 
2698 /**
2699  * Paints walls and rooms of lower levels or upper levels to help the user draw in the given level.
2700  * @param {Graphics2D} g2D
2701  * @param {Level} level
2702  * @param {number} planScale
2703  * @param {string} backgroundColor
2704  * @param {string} foregroundColor
2705  * @private
2706  */
2707 PlanComponent.prototype.paintOtherLevels = function(g2D, level, planScale, backgroundColor, foregroundColor) {
2708   var plan = this;
2709   var levels = this.home.getLevels();
2710   if (levels.length 
2711       && level != null) {
2712     var level0 = levels[0].getElevation() === level.getElevation();
2713     var otherLevels = null;
2714     if (this.otherLevelsRoomsCache == null 
2715         || this.otherLevelsWallsCache == null) {
2716       var selectedLevelIndex = levels.indexOf(level);
2717       otherLevels = [];
2718       if (level0) {
2719         var nextElevationLevelIndex = selectedLevelIndex;
2720         while (++nextElevationLevelIndex < levels.length 
2721             && levels[nextElevationLevelIndex].getElevation() === level.getElevation()) {
2722         }
2723         if (nextElevationLevelIndex < levels.length) {
2724           var nextLevel = levels[nextElevationLevelIndex];
2725           var nextElevation = nextLevel.getElevation();
2726           do {
2727             if (nextLevel.isViewable()) {
2728               otherLevels.push(nextLevel);
2729             }
2730           } while (++nextElevationLevelIndex < levels.length 
2731               && (nextLevel = levels[nextElevationLevelIndex]).getElevation() === nextElevation);
2732         }
2733       } else {
2734         var previousElevationLevelIndex = selectedLevelIndex;
2735         while (--previousElevationLevelIndex >= 0 
2736             && levels[previousElevationLevelIndex].getElevation() === level.getElevation()) {
2737         }
2738         if (previousElevationLevelIndex >= 0) {
2739           var previousLevel = levels[previousElevationLevelIndex];
2740           var previousElevation = previousLevel.getElevation();
2741           do {
2742             if (previousLevel.isViewable()) {
2743               otherLevels.push(previousLevel);
2744             }
2745           } while (--previousElevationLevelIndex >= 0 
2746               && (previousLevel = levels[previousElevationLevelIndex]).getElevation() === previousElevation);
2747         }
2748       }
2749       if (this.otherLevelsRoomsCache == null) {
2750         if (otherLevels.length !== 0) {
2751           var otherLevelsRooms = [];
2752           this.home.getRooms().forEach(function(room) {
2753               otherLevels.forEach(function(otherLevel) {
2754                   if (room.getLevel() === otherLevel 
2755                       && (level0 && room.isFloorVisible() 
2756                           || !level0 && room.isCeilingVisible())) {
2757                     otherLevelsRooms.push(room);
2758                   }
2759                 });
2760             });
2761           if (otherLevelsRooms.length > 0) {
2762             this.otherLevelsRoomAreaCache = this.getItemsArea(otherLevelsRooms);
2763             this.otherLevelsRoomsCache = otherLevelsRooms;
2764           }
2765         }
2766         if (this.otherLevelsRoomsCache == null) {
2767           this.otherLevelsRoomsCache = [];
2768         }
2769       }
2770       if (this.otherLevelsWallsCache == null) {
2771         if (otherLevels.length !== 0) {
2772           var otherLevelswalls = [];
2773           this.home.getWalls().forEach(function(wall) {
2774             if (!plan.isViewableAtLevel(wall, level)) {
2775               otherLevels.forEach(function(otherLevel) {
2776                   if (wall.getLevel() === otherLevel) {
2777                     otherLevelswalls.push(wall);
2778                   }
2779                 });
2780             }
2781           });
2782           if (otherLevelswalls.length > 0) {
2783             this.otherLevelsWallAreaCache = this.getItemsArea(otherLevelswalls);
2784             this.otherLevelsWallsCache = otherLevelswalls;
2785           }
2786         }
2787       }
2788       if (this.otherLevelsWallsCache == null) {
2789         this.otherLevelsWallsCache = [];
2790       }
2791     }
2792     if (this.otherLevelsRoomsCache.length !== 0) {
2793       var oldComposite = this.setTransparency(g2D, 
2794           this.preferences.isGridVisible() ? 0.2 : 0.1);
2795       g2D.setPaint("#808080");
2796       g2D.fill(this.otherLevelsRoomAreaCache);
2797       g2D.setAlpha(oldComposite);
2798     }
2799     if (this.otherLevelsWallsCache.length !== 0) {
2800       var oldComposite = this.setTransparency(g2D, 
2801           this.preferences.isGridVisible() ? 0.2 : 0.1);
2802       this.fillAndDrawWallsArea(g2D, this.otherLevelsWallAreaCache, planScale, 
2803           this.getWallPaint(g2D, planScale, backgroundColor, foregroundColor, this.preferences.getNewWallPattern()), 
2804           foregroundColor, PlanComponent.PaintMode.PAINT);
2805       g2D.setAlpha(oldComposite);
2806     }
2807   }
2808 }
2809 
2810 /**
2811  * Sets the transparency composite to the given percentage and returns the old composite.
2812  * @param {Graphics2D} g2D
2813  * @param {number} alpha
2814  * @return {Object}
2815  * @private
2816  */
2817 PlanComponent.prototype.setTransparency = function(g2D, alpha) {
2818   var oldAlpha = g2D.getAlpha();
2819   g2D.setAlpha(alpha);
2820   return oldAlpha;
2821 }
2822 
2823 /**
2824  * Paints background grid lines.
2825  * @param {Graphics2D} g2D
2826  * @param {number} gridScale
2827  * @private
2828  */
2829 PlanComponent.prototype.paintGrid = function(g2D, gridScale) {
2830   var gridSize = this.getGridSize(gridScale);
2831   var mainGridSize = this.getMainGridSize(gridScale);
2832   var planBounds = this.getPlanBounds();
2833   var xMin = planBounds.getMinX() - PlanComponent.MARGIN;
2834   var yMin = planBounds.getMinY() - PlanComponent.MARGIN;
2835   var xMax = this.convertXPixelToModel(Math.max(this.getWidth(), this.canvas.clientWidth));
2836   var yMax = this.convertYPixelToModel(Math.max(this.getHeight(), this.canvas.clientHeight));
2837   this.paintGridLines(g2D, gridScale, xMin, xMax, yMin, yMax, gridSize, mainGridSize);
2838 }
2839 
2840 /**
2841  * Paints background grid lines from <code>xMin</code> to <code>xMax</code>
2842  * and <code>yMin</code> to <code>yMax</code>.
2843  * @param {Graphics2D} g2D
2844  * @param {number} gridScale
2845  * @param {number} xMin
2846  * @param {number} xMax
2847  * @param {number} yMin
2848  * @param {number} yMax
2849  * @param {number} gridSize
2850  * @param {number} mainGridSize
2851  * @private
2852  */
2853 PlanComponent.prototype.paintGridLines = function(g2D, gridScale, xMin, xMax, yMin, yMax, gridSize, mainGridSize) {
2854   g2D.setColor(this.getGridColor());
2855   g2D.setStroke(new java.awt.BasicStroke(0.5 / gridScale));
2856   for (var x = ((xMin / gridSize) | 0) * gridSize; x < xMax; x += gridSize) {
2857     g2D.draw(new java.awt.geom.Line2D.Double(x, yMin, x, yMax));
2858   }
2859   for (var y = ((yMin / gridSize) | 0) * gridSize; y < yMax; y += gridSize) {
2860     g2D.draw(new java.awt.geom.Line2D.Double(xMin, y, xMax, y));
2861   }
2862   if (mainGridSize !== gridSize) {
2863     g2D.setStroke(new java.awt.BasicStroke(1.5 / gridScale, 
2864         java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_BEVEL));
2865     for (var x = ((xMin / mainGridSize) | 0) * mainGridSize; x < xMax; x += mainGridSize) {
2866       g2D.draw(new java.awt.geom.Line2D.Double(x, yMin, x, yMax));
2867     }
2868     for (var y = ((yMin / mainGridSize) | 0) * mainGridSize; y < yMax; y += mainGridSize) {
2869       g2D.draw(new java.awt.geom.Line2D.Double(xMin, y, xMax, y));
2870     }
2871   }
2872 }
2873 
2874 /**
2875  * Returns the color used to paint the grid. 
2876  * @private 
2877  */
2878 PlanComponent.prototype.getGridColor = function() {
2879   if (this.gridColor == null) {
2880     // compute the gray color in between background and foreground colors
2881     var background = ColorTools.styleToInteger(this.canvas.style.backgroundColor);
2882     var foreground = ColorTools.styleToInteger(this.canvas.style.color);
2883     var gridColorComponent = ((((background & 0xFF) + (foreground & 0xFF) + (background & 0xFF00) + (foreground & 0xFF00) + (background & 0xFF0000) + (foreground & 0xFF0000)) / 6) | 0) & 0xFF;
2884     this.gridColor = ColorTools.integerToHexadecimalString(gridColorComponent + (gridColorComponent << 8) + (gridColorComponent << 16));
2885   }
2886   return this.gridColor;
2887 }
2888 
2889 /**
2890  * Returns the space between main lines grid.
2891  * @param {number} gridScale
2892  * @return {number}
2893  * @private
2894  */
2895 PlanComponent.prototype.getMainGridSize = function(gridScale) {
2896   var mainGridSizes;
2897   var lengthUnit = this.preferences.getLengthUnit();
2898   if (lengthUnit.isMetric()) {
2899     mainGridSizes = [100, 200, 500, 1000, 2000, 5000, 10000];
2900   } else {
2901     var oneFoot = 2.54 * 12;
2902     mainGridSizes = [oneFoot, 3 * oneFoot, 6 * oneFoot, 
2903                      12 * oneFoot, 24 * oneFoot, 48 * oneFoot, 96 * oneFoot, 192 * oneFoot, 384 * oneFoot];
2904   }
2905   var mainGridSize = mainGridSizes[0];
2906   for (var i = 1; i < mainGridSizes.length && mainGridSize * gridScale < 50; i++) {
2907     mainGridSize = mainGridSizes[i];
2908   }
2909   return mainGridSize;
2910 }
2911 
2912 /**
2913  * Returns the space between lines grid.
2914  * @param {number} gridScale
2915  * @return {number}
2916  * @private
2917  */
2918 PlanComponent.prototype.getGridSize = function(gridScale) {
2919   var gridSizes;
2920   var lengthUnit = this.preferences.getLengthUnit();
2921   if (lengthUnit.isMetric()) {
2922     gridSizes = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000];
2923   } else {
2924     var oneFoot = 2.54 * 12;
2925     gridSizes = [2.54, 5.08, 7.62, 15.24, oneFoot, 3 * oneFoot, 6 * oneFoot, 
2926                  12 * oneFoot, 24 * oneFoot, 48 * oneFoot, 96 * oneFoot, 192 * oneFoot, 384 * oneFoot];
2927   }
2928   var gridSize = gridSizes[0];
2929   for (var i = 1; i < gridSizes.length && gridSize * gridScale < 10; i++) {
2930     gridSize = gridSizes[i];
2931   }
2932   return gridSize;
2933 }
2934 
2935 /**
2936  * Paints plan items at the given <code>level</code>.
2937  * @throws InterruptedIOException if painting was interrupted (may happen only
2938  * if <code>paintMode</code> is equal to <code>PaintMode.EXPORT</code>).
2939  * @param {Graphics2D} g2D
2940  * @param {Level} level
2941  * @param {number} gridScale
2942  * @param {PlanComponent.PaintMode} paintMode
2943  * @private
2944  */
2945 PlanComponent.prototype.paintContent = function(g2D, level, planScale, paintMode) {
2946   var backgroundColor = this.getBackgroundColor(paintMode);
2947   var foregroundColor = this.getForegroundColor(paintMode);
2948   if (this.backgroundPainted) {
2949     this.paintBackgroundImage(g2D, level, paintMode);
2950     if (paintMode === PlanComponent.PaintMode.PAINT) {
2951       this.paintOtherLevels(g2D, level, planScale, backgroundColor, foregroundColor);
2952       if (this.preferences.isGridVisible()) {
2953         this.paintGrid(g2D, planScale);
2954       }
2955     }
2956   }
2957   
2958   if (level == this.home.getSelectedLevel()) {
2959     // Call deprecated implementation in case a subclass did override paintHomeItems
2960     this.paintHomeItems(g2D, planScale, backgroundColor, foregroundColor, paintMode);
2961   } else {
2962     this.paintHomeItems(g2D, level, planScale, backgroundColor, foregroundColor, paintMode);
2963   }
2964 
2965   if (paintMode === PlanComponent.PaintMode.PAINT) {
2966     var selectedItems = this.home.getSelectedItems();
2967     var selectionColor = this.getSelectionColor();
2968     var furnitureOutlineColor = this.getFurnitureOutlineColor();
2969     var selectionOutlinePaint = ColorTools.toRGBAStyle(selectionColor, 0.5);
2970     var selectionOutlineStroke = new java.awt.BasicStroke(6 / planScale, 
2971         java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND);
2972     var dimensionLinesSelectionOutlineStroke = new java.awt.BasicStroke(4 / planScale, 
2973         java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND);
2974     var locationFeedbackStroke = new java.awt.BasicStroke(
2975         1 / planScale, java.awt.BasicStroke.CAP_SQUARE, java.awt.BasicStroke.JOIN_BEVEL, 0, 
2976         [20 / planScale, 5 / planScale, 5 / planScale, 5 / planScale], 4 / planScale);
2977     
2978     this.paintCamera(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
2979         planScale, backgroundColor, foregroundColor);
2980     
2981     if (this.alignedObjectClass != null) {
2982       if (Wall === this.alignedObjectClass) {
2983         this.paintWallAlignmentFeedback(g2D, this.alignedObjectFeedback, level, this.locationFeeback, this.showPointFeedback, 
2984             selectionColor, locationFeedbackStroke, planScale, 
2985             selectionOutlinePaint, selectionOutlineStroke);
2986       } else if (Room === this.alignedObjectClass) {
2987         this.paintRoomAlignmentFeedback(g2D, this.alignedObjectFeedback, level, this.locationFeeback, this.showPointFeedback, 
2988             selectionColor, locationFeedbackStroke, planScale, 
2989             selectionOutlinePaint, selectionOutlineStroke);
2990       } else if (Polyline === this.alignedObjectClass) {
2991         if (this.showPointFeedback) {
2992           this.paintPointFeedback(g2D, this.locationFeeback, selectionColor, planScale, selectionOutlinePaint, selectionOutlineStroke);
2993         }
2994       } else if (DimensionLine === this.alignedObjectClass) {
2995         this.paintDimensionLineAlignmentFeedback(g2D, this.alignedObjectFeedback, level, this.locationFeeback, this.showPointFeedback, 
2996             selectionColor, locationFeedbackStroke, planScale, 
2997             selectionOutlinePaint, selectionOutlineStroke);
2998       }
2999     }
3000     if (this.centerAngleFeedback != null) {
3001       this.paintAngleFeedback(g2D, this.centerAngleFeedback, this.point1AngleFeedback, this.point2AngleFeedback, 
3002           planScale, selectionColor);
3003     }
3004     if (this.dimensionLinesFeedback != null) {
3005       var emptySelection = [];
3006       this.paintDimensionLines(g2D, this.dimensionLinesFeedback, emptySelection, level,
3007           null, null, null, locationFeedbackStroke, planScale, 
3008           backgroundColor, selectionColor, paintMode, true);
3009     }
3010     
3011     if (this.draggedItemsFeedback != null) {
3012       this.paintDimensionLines(g2D, Home.getDimensionLinesSubList(this.draggedItemsFeedback), this.draggedItemsFeedback, level,
3013           selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, null, 
3014           locationFeedbackStroke, planScale, backgroundColor, foregroundColor, paintMode, false);
3015       this.paintLabels(g2D, Home.getLabelsSubList(this.draggedItemsFeedback), this.draggedItemsFeedback, level,
3016           selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, null, 
3017           planScale, foregroundColor, paintMode);
3018       this.paintRoomsOutline(g2D, this.draggedItemsFeedback, level, selectionOutlinePaint, selectionOutlineStroke, null, 
3019           planScale, foregroundColor);
3020       this.paintWallsOutline(g2D, this.draggedItemsFeedback, level, selectionOutlinePaint, selectionOutlineStroke, null, 
3021           planScale, foregroundColor);
3022       this.paintFurniture(g2D, Home.getFurnitureSubList(this.draggedItemsFeedback), selectedItems, level, planScale, null, 
3023           foregroundColor, furnitureOutlineColor, paintMode, false);
3024       this.paintFurnitureOutline(g2D, this.draggedItemsFeedback, level, selectionOutlinePaint, selectionOutlineStroke, null, 
3025           planScale, foregroundColor);
3026     }
3027     
3028     this.paintRectangleFeedback(g2D, selectionColor, planScale);
3029   }
3030 }
3031 
3032 /**
3033  * Paints home items at the given scale, and with background and foreground colors.
3034  * Outline around selected items will be painted only under <code>PAINT</code> mode.
3035  * @param {Graphics2D} g
3036  * @param {Level} level
3037  * @param {number} planScale
3038  * @param {string} backgroundColor
3039  * @param {string} foregroundColor
3040  * @param {PlanComponent.PaintMode} paintMode
3041  */
3042 PlanComponent.prototype.paintHomeItems = function(g2D, level, planScale, backgroundColor, foregroundColor, paintMode) {
3043   if (paintMode === undefined) {
3044     // 5 parameters
3045     paintMode = foregroundColor;  
3046     foregroundColor = backgroundColor; 
3047     backgroundColor = planScale; 
3048     planScale = level; 
3049     level = this.home.getSelectedLevel(); 
3050   }
3051   var plan = this;
3052   var selectedItems = this.home.getSelectedItems();
3053   if (this.sortedLevelFurniture == null) {
3054     this.sortedLevelFurniture = [];
3055     this.home.getFurniture().forEach(function(piece) {
3056         if (plan.isViewableAtLevel(piece, level)) {
3057           plan.sortedLevelFurniture.push(piece);
3058         }
3059       });
3060     CoreTools.sortArray(this.sortedLevelFurniture, {
3061         compare: function(piece1, piece2) {
3062           return (piece1.getGroundElevation() - piece2.getGroundElevation());
3063         }
3064       });
3065   }
3066   var selectionColor = this.getSelectionColor();
3067   var selectionOutlinePaint = ColorTools.toRGBAStyle(selectionColor, 0.5);
3068   var selectionOutlineStroke = new java.awt.BasicStroke(6 / planScale, 
3069       java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND);
3070   var dimensionLinesSelectionOutlineStroke = new java.awt.BasicStroke(4 / planScale, 
3071       java.awt.BasicStroke.CAP_ROUND, java.awt.BasicStroke.JOIN_ROUND);
3072   var locationFeedbackStroke = new java.awt.BasicStroke(
3073       1 / planScale, java.awt.BasicStroke.CAP_SQUARE, java.awt.BasicStroke.JOIN_BEVEL, 0, 
3074       [20 / planScale, 5 / planScale, 5 / planScale, 5 / planScale], 4 / planScale);
3075 
3076   this.paintCompass(g2D, selectedItems, planScale, foregroundColor, paintMode);
3077   
3078   this.paintRooms(g2D, selectedItems, level, planScale, foregroundColor, paintMode);
3079   
3080   this.paintWalls(g2D, selectedItems, level, planScale, backgroundColor, foregroundColor, paintMode);
3081   
3082   this.paintFurniture(g2D, this.sortedLevelFurniture, selectedItems, level,
3083       planScale, backgroundColor, foregroundColor, this.getFurnitureOutlineColor(), paintMode, true);
3084   
3085   this.paintPolylines(g2D, this.home.getPolylines(), selectedItems, level,
3086       selectionOutlinePaint, selectionColor, planScale, foregroundColor, paintMode);
3087   
3088   this.paintDimensionLines(g2D, this.home.getDimensionLines(), selectedItems, level,
3089       selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, selectionColor, 
3090       locationFeedbackStroke, planScale, backgroundColor, foregroundColor, paintMode, false);
3091   
3092   this.paintRoomsNameAndArea(g2D, selectedItems, planScale, foregroundColor, paintMode);
3093   
3094   this.paintFurnitureName(g2D, this.sortedLevelFurniture, selectedItems, planScale, foregroundColor, paintMode);
3095   
3096   this.paintLabels(g2D, this.home.getLabels(), selectedItems, level,
3097       selectionOutlinePaint, dimensionLinesSelectionOutlineStroke, 
3098       selectionColor, planScale, foregroundColor, paintMode);
3099   
3100   if (paintMode === PlanComponent.PaintMode.PAINT 
3101       && this.selectedItemsOutlinePainted) {
3102     this.paintCompassOutline(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
3103         planScale, foregroundColor);
3104     this.paintRoomsOutline(g2D, selectedItems, level, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
3105         planScale, foregroundColor);
3106     this.paintWallsOutline(g2D, selectedItems, level, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
3107         planScale, foregroundColor);
3108     this.paintFurnitureOutline(g2D, selectedItems, level, selectionOutlinePaint, selectionOutlineStroke, selectionColor, 
3109         planScale, foregroundColor);
3110   }
3111 }
3112 
3113 /**
3114  * Returns the color used to draw selection outlines.
3115  * @return {string}
3116  */
3117 PlanComponent.prototype.getSelectionColor = function() {
3118   return PlanComponent.getDefaultSelectionColor(this);
3119 }
3120 
3121 /**
3122  * Returns the default color used to draw selection outlines.
3123  * Note that the default selection color may be forced using CSS by setting backgroundColor with the ::selection selector.
3124  * In case the browser does not support the ::selection selector, one can force the selectio color in JavaScript by setting PlanComponent.DEFAULT_SELECTION_COLOR.
3125  * @param {PlanComponent} planComponent
3126  * @return {string}
3127  */
3128 PlanComponent.getDefaultSelectionColor = function(planComponent) {
3129   if (PlanComponent.DEFAULT_SELECTION_COLOR == null) {
3130     var color = window.getComputedStyle(planComponent.container, "::selection").backgroundColor;
3131     if (color.indexOf("rgb") === -1 
3132         || ColorTools.isTransparent(color) 
3133         || color == window.getComputedStyle(planComponent.container).backgroundColor) {
3134       planComponent.container.style.color = "Highlight";
3135       color = window.getComputedStyle(planComponent.container).color;
3136     }
3137     if (color.indexOf("rgb") === -1 
3138         || ColorTools.isTransparent(color) 
3139         || color == window.getComputedStyle(planComponent.container).backgroundColor) {
3140       PlanComponent.DEFAULT_SELECTION_COLOR = "#0042E0";
3141     } else {
3142       PlanComponent.DEFAULT_SELECTION_COLOR = ColorTools.styleToHexadecimalString(color);
3143     }
3144   }
3145   return PlanComponent.DEFAULT_SELECTION_COLOR;
3146 }
3147 
3148 /**
3149  * Returns the color used to draw furniture outline of
3150  * the shape where a user can click to select a piece of furniture.
3151  * @return {string}
3152  */
3153 PlanComponent.prototype.getFurnitureOutlineColor = function() {
3154   return ColorTools.toRGBAStyle(this.getForeground(), 0.33);
3155 }
3156 
3157 /**
3158  * Paints rooms.
3159  * @param {Graphics2D} g2D
3160  * @param {Object[]} selectedItems
3161  * @param {Level} level
3162  * @param {number} planScale
3163  * @param {string} foregroundColor
3164  * @param {PlanComponent.PaintMode} paintMode
3165  * @private
3166  */
3167 PlanComponent.prototype.paintRooms = function(g2D, selectedItems, level, planScale, foregroundColor, paintMode) {
3168   var plan = this;
3169   if (this.sortedLevelRooms == null) {
3170     this.sortedLevelRooms = [];
3171     this.home.getRooms().forEach(function(room) {
3172         if (plan.isViewableAtLevel(room, level)) {
3173           plan.sortedLevelRooms.push(room);
3174         }
3175       });
3176     CoreTools.sortArray(this.sortedLevelRooms, {
3177         compare: function(room1, room2) {
3178           if (room1.isFloorVisible() === room2.isFloorVisible() 
3179               && room1.isCeilingVisible() === room2.isCeilingVisible()) {
3180             return 0;
3181           } else if (!room1.isFloorVisible() && !room1.isCeilingVisible()
3182                      || room1.isFloorVisible() && room2.isCeilingVisible()) {
3183             return -1;
3184           } else {
3185             return 1;
3186           }
3187         }
3188       });
3189   }
3190   var defaultFillPaint = paintMode === PlanComponent.PaintMode.PRINT
3191       ? "#000000" 
3192       : "#808080";
3193   g2D.setStroke(new java.awt.BasicStroke(this.getStrokeWidth(Room, paintMode) / planScale));
3194   for (var i = 0; i < this.sortedLevelRooms.length; i++) {
3195     var room = plan.sortedLevelRooms[i];
3196     var selectedRoom = selectedItems.indexOf(room) >= 0;
3197     if (paintMode !== PlanComponent.PaintMode.CLIPBOARD 
3198         || selectedRoom) {
3199       g2D.setPaint(defaultFillPaint);
3200       var textureAngle = 0;
3201       var textureScaleX = 1;
3202       var textureScaleY = 1;
3203       var textureOffsetX = 0;
3204       var textureOffsetY = 0;
3205       var floorTexture = null;
3206       if (plan.preferences.isRoomFloorColoredOrTextured() && room.isFloorVisible()) {
3207         if (room.getFloorColor() != null) {
3208           g2D.setPaint(ColorTools.integerToHexadecimalString(room.getFloorColor()));
3209         } else {
3210           floorTexture = room.getFloorTexture();
3211           if (floorTexture != null) {
3212             if (plan.floorTextureImagesCache == null) {
3213               plan.floorTextureImagesCache = {};
3214             }
3215             var textureImage = plan.floorTextureImagesCache[floorTexture.getImage().getURL()];
3216             if (textureImage == null) {
3217               textureImage = PlanComponent.WAIT_TEXTURE_IMAGE;
3218               plan.floorTextureImagesCache[floorTexture.getImage().getURL()] = textureImage;
3219               var waitForTexture = paintMode !== PlanComponent.PaintMode.PAINT;
3220               TextureManager.getInstance().loadTexture(floorTexture.getImage(), waitForTexture, {
3221                   floorTexture: floorTexture,
3222                   textureUpdated: function(texture) {
3223                     plan.floorTextureImagesCache[this.floorTexture.getImage().getURL()] = texture;
3224                     if (!waitForTexture) {
3225                       plan.repaint();
3226                     }
3227                   },
3228                   textureError: function() {
3229                     plan.floorTextureImagesCache[this.floorTexture.getImage().getURL()] = PlanComponent.ERROR_TEXTURE_IMAGE;
3230                   },
3231                   progression: function() { }
3232                 });
3233             }
3234             if (room.getFloorTexture().isFittingArea()) {
3235               var min = room.getBoundsMinimumCoordinates();
3236               var max = room.getBoundsMaximumCoordinates();
3237               textureScaleX = (max[0] - min[0]) / textureImage.naturalWidth;
3238               textureScaleY = (max[1] - min[1]) / textureImage.naturalHeight;
3239               textureOffsetX = min[0] / textureScaleX;
3240               textureOffsetY = min[1] / textureScaleY;
3241             } else {
3242               var textureWidth = floorTexture.getWidth();
3243               var textureHeight = floorTexture.getHeight();
3244               if (textureWidth === -1 || textureHeight === -1) {
3245                 textureWidth = 100;
3246                 textureHeight = 100;
3247               }
3248               var textureScale = floorTexture.getScale();
3249               textureScaleX = (textureWidth * textureScale) / textureImage.naturalWidth;
3250               textureScaleY = (textureHeight * textureScale) / textureImage.naturalHeight;
3251               textureAngle = floorTexture.getAngle();
3252               var cosAngle = Math.cos(textureAngle);
3253               var sinAngle = Math.sin(textureAngle);
3254               textureOffsetX = (floorTexture.getXOffset() * textureImage.naturalWidth * cosAngle - floorTexture.getYOffset() * textureImage.height * sinAngle);
3255               textureOffsetY = (-floorTexture.getXOffset() * textureImage.naturalWidth * sinAngle - floorTexture.getYOffset() * textureImage.height * cosAngle);
3256             }
3257             g2D.setPaint(g2D.createPattern(textureImage));
3258           }
3259         }
3260       }
3261       
3262       var oldComposite = plan.setTransparency(g2D, room.isFloorVisible() ? 0.75 : 0.5);
3263       var transform = null;
3264       if (floorTexture != null) {
3265         g2D.scale(textureScaleX, textureScaleY);
3266         g2D.rotate(textureAngle, 0, 0);
3267         g2D.translate(textureOffsetX, textureOffsetY);
3268         transform = java.awt.geom.AffineTransform.getTranslateInstance(-textureOffsetX, -textureOffsetY);
3269         transform.rotate(-textureAngle, 0, 0);
3270         transform.scale(1 / textureScaleX, 1 / textureScaleY);
3271       }
3272       var roomShape = ShapeTools.getShape(room.getPoints(), true, transform);
3273       plan.fillShape(g2D, roomShape, paintMode);
3274       if (floorTexture != null) {
3275         g2D.translate(-textureOffsetX, -textureOffsetY);
3276         g2D.rotate(-textureAngle, 0, 0);
3277         g2D.scale(1 / textureScaleX, 1 / textureScaleY);
3278         roomShape = ShapeTools.getShape(room.getPoints(), true);
3279       }
3280       g2D.setAlpha(oldComposite);
3281       g2D.setPaint(foregroundColor);
3282       g2D.draw(roomShape);
3283     }
3284   }
3285 }
3286 
3287 /**
3288  * Fills the given <code>shape</code>.
3289  * @param {Graphics2D} g2D
3290  * @param {Object} shape
3291  * @param {PlanComponent.PaintMode} paintMode
3292  * @private
3293  */
3294 PlanComponent.prototype.fillShape = function(g2D, shape, paintMode) {
3295   g2D.fill(shape);
3296 }
3297 
3298 /**
3299  * Returns <code>true</code> if <code>TextureManager</code> can be used to manage textures.
3300  * @return {boolean}
3301  * @private
3302  */
3303 PlanComponent.isTextureManagerAvailable = function() {
3304   return true;
3305 }
3306 
3307 /**
3308  * Paints rooms name and area.
3309  * @param {Graphics2D} g2D
3310  * @param {Object[]} selectedItems
3311  * @param {number} planScale
3312  * @param {string} foregroundColor
3313  * @param {PlanComponent.PaintMode} paintMode
3314  * @private
3315  */
3316 PlanComponent.prototype.paintRoomsNameAndArea = function(g2D, selectedItems, planScale, foregroundColor, paintMode) {
3317   var plan = this;
3318   g2D.setPaint(foregroundColor);
3319   var previousFont = g2D.getFont();
3320   this.sortedLevelRooms.forEach(function(room) {
3321       var selectedRoom = (selectedItems.indexOf(room) >= 0);
3322       if (paintMode !== PlanComponent.PaintMode.CLIPBOARD 
3323           || selectedRoom) {
3324         var xRoomCenter = room.getXCenter();
3325         var yRoomCenter = room.getYCenter();
3326         var name = room.getName();
3327         if (name != null) {
3328           name = name.trim();
3329           if (name.length > 0) {
3330             plan.paintText(g2D, room.constructor, name, room.getNameStyle(), null, 
3331                 xRoomCenter + room.getNameXOffset(), 
3332                 yRoomCenter + room.getNameYOffset(), 
3333                 room.getNameAngle(), previousFont);
3334           }
3335         }
3336         if (room.isAreaVisible()) {
3337           var area = room.getArea();
3338           if (area > 0.01) {
3339             var areaText = plan.preferences.getLengthUnit().getAreaFormatWithUnit().format(area);
3340             plan.paintText(g2D, room.constructor, areaText, room.getAreaStyle(), null, 
3341                 xRoomCenter + room.getAreaXOffset(), 
3342                 yRoomCenter + room.getAreaYOffset(), 
3343                 room.getAreaAngle(), previousFont);
3344           }
3345         }
3346       }
3347     });
3348   g2D.setFont(previousFont);
3349 }
3350 
3351 /**
3352  * Paints the given <code>text</code> centered at the point (<code>x</code>,<code>y</code>).
3353  * @param {Graphics2D} g2D
3354  * @param {Object} selectableClass
3355  * @param {string} text
3356  * @param {TextStyle} style
3357  * @param {number} outlineColor
3358  * @param {number} x
3359  * @param {number} y
3360  * @param {number} angle
3361  * @param {string} defaultFont
3362  * @private
3363  */
3364 PlanComponent.prototype.paintText = function(g2D, selectableClass, text, style, outlineColor, x, y, angle, defaultFont) {
3365   var previousTransform = g2D.getTransform();
3366   g2D.translate(x, y);
3367   g2D.rotate(angle);
3368   if (style == null) {
3369     style = this.preferences.getDefaultTextStyle(selectableClass);
3370   }
3371   var fontMetrics = this.getFontMetrics(defaultFont, style);
3372   var lines = text.replace(/\n*$/, "").split("\n");
3373   var lineWidths = new Array(lines.length);
3374   var textWidth = -3.4028235E38;
3375   for (var i = 0; i < lines.length; i++) {
3376     lineWidths[i] = fontMetrics.getStringBounds(lines[i]).getWidth();
3377     textWidth = Math.max(lineWidths[i], textWidth);
3378   }
3379   var stroke = null;
3380   var font;
3381   if (outlineColor != null) {
3382     stroke = new java.awt.BasicStroke(style.getFontSize() * 0.05);
3383     // Call directly the overloaded deriveStyle method that takes a float parameter 
3384     // to avoid confusion with the one that takes a TextStyle.Alignment parameter
3385     var outlineStyle = style.deriveStyle$float(style.getFontSize() - stroke.getLineWidth());
3386     font = this.getFont(defaultFont, outlineStyle);
3387     g2D.setStroke(stroke);
3388   } else {
3389     font = this.getFont(defaultFont, style);
3390   }
3391   g2D.setFont(font);
3392   
3393   for (var i = lines.length - 1; i >= 0; i--) {
3394     var line = lines[i];
3395     var translationX = void 0;
3396     if (style.getAlignment() === TextStyle.Alignment.LEFT) {
3397       translationX = 0;
3398     } else if (style.getAlignment() === TextStyle.Alignment.RIGHT) {
3399       translationX = -lineWidths[i];
3400     } else {
3401       translationX = -lineWidths[i] / 2;
3402     }
3403     if (outlineColor != null) {
3404       translationX += stroke.getLineWidth() / 2;
3405     }
3406     g2D.translate(translationX, 0);
3407     if (outlineColor != null) {
3408       var defaultColor = g2D.getColor();
3409       g2D.setColor(ColorTools.integerToHexadecimalString(outlineColor));
3410       g2D.drawStringOutline(line, 0, 0);
3411       g2D.setColor(defaultColor);
3412     }
3413     g2D.drawString(line, 0, 0);
3414     g2D.translate(-translationX, -fontMetrics.getHeight());
3415   }
3416   g2D.setTransform(previousTransform);
3417 }
3418 
3419 /**
3420  * Paints the outline of rooms among <code>items</code> and indicators if
3421  * <code>items</code> contains only one room and indicator paint isn't <code>null</code>.
3422  * @param {Graphics2D} g2D
3423  * @param {Object[]} items
3424  * @param {Level} level
3425  * @param {string|CanvasPattern} selectionOutlinePaint
3426  * @param {java.awt.BasicStroke} selectionOutlineStroke
3427  * @param {string|CanvasPattern} indicatorPaint
3428  * @param {number} planScale
3429  * @param {string} foregroundColor
3430  * @private
3431  */
3432 PlanComponent.prototype.paintRoomsOutline = function(g2D, items, level, selectionOutlinePaint, selectionOutlineStroke, indicatorPaint, planScale, foregroundColor) {
3433   var rooms = Home.getRoomsSubList(items);
3434   var previousTransform = g2D.getTransform();
3435   var scaleInverse = 1 / planScale;
3436   for (var i = 0; i < rooms.length; i++) {
3437     var room = rooms[i];
3438     if (this.isViewableAtLevel(room, level)) {
3439       g2D.setPaint(selectionOutlinePaint);
3440       g2D.setStroke(selectionOutlineStroke);
3441       g2D.draw(ShapeTools.getShape(room.getPoints(), true, null));
3442       
3443       if (indicatorPaint != null) {
3444         g2D.setPaint(indicatorPaint);
3445         room.getPoints().forEach(function(point) {
3446             g2D.translate(point[0], point[1]);
3447             g2D.scale(scaleInverse, scaleInverse);
3448             g2D.setStroke(PlanComponent.POINT_STROKE);
3449             g2D.fill(PlanComponent.WALL_POINT);
3450             g2D.setTransform(previousTransform);
3451           });
3452       }
3453     }
3454   }
3455   
3456   g2D.setPaint(foregroundColor);
3457   g2D.setStroke(new java.awt.BasicStroke(this.getStrokeWidth(Room, PlanComponent.PaintMode.PAINT) / planScale));
3458   for (var i = 0; i < rooms.length; i++) {
3459     var room = rooms[i];
3460     if (this.isViewableAtLevel(room, level)) {
3461       g2D.draw(ShapeTools.getShape(room.getPoints(), true, null));
3462     }
3463   }
3464   
3465   if (items.length === 1 
3466       && rooms.length === 1 
3467       && indicatorPaint != null) {
3468     var selectedRoom = rooms[0];
3469     if (this.isViewableAtLevel(room, level)) {
3470       g2D.setPaint(indicatorPaint);
3471       this.paintPointsResizeIndicators(g2D, selectedRoom, indicatorPaint, planScale, true, 0, 0, true);
3472       this.paintRoomNameOffsetIndicator(g2D, selectedRoom, indicatorPaint, planScale);
3473       this.paintRoomAreaOffsetIndicator(g2D, selectedRoom, indicatorPaint, planScale);
3474     }
3475   }
3476 }
3477 
3478 /**
3479  * Paints resize indicators on selectable <code>item</code>.
3480  * @param {Graphics2D} g2D
3481  * @param {Object} item
3482  * @param {string|CanvasPattern} indicatorPaint
3483  * @param {number} planScale
3484  * @param {boolean} closedPath
3485  * @param {number} angleAtStart
3486  * @param {number} angleAtEnd
3487  * @param {boolean} orientateIndicatorOutsideShape
3488  * @private
3489  */
3490 PlanComponent.prototype.paintPointsResizeIndicators = function(g2D, item, indicatorPaint, planScale, closedPath, angleAtStart, angleAtEnd, orientateIndicatorOutsideShape) {
3491   if (this.resizeIndicatorVisible) {
3492     g2D.setPaint(indicatorPaint);
3493     g2D.setStroke(PlanComponent.INDICATOR_STROKE);
3494     var previousTransform = g2D.getTransform();
3495     var scaleInverse = 1 / planScale;
3496     var points = item.getPoints();
3497     var resizeIndicator = this.getIndicator(item, PlanComponent.IndicatorType.RESIZE);
3498     for (var i = 0; i < points.length; i++) {
3499       var point = points[i];
3500       g2D.translate(point[0], point[1]);
3501       g2D.scale(scaleInverse, scaleInverse);
3502       var previousPoint = i === 0 
3503           ? points[points.length - 1]
3504           : points[i - 1];
3505       var nextPoint = i === points.length - 1 
3506           ? points[0] 
3507           : points[i + 1];
3508       var angle = void 0;
3509       if (closedPath || (i > 0 && i < points.length - 1)) {
3510         var distance1 = java.awt.geom.Point2D.distance(
3511             previousPoint[0], previousPoint[1], point[0], point[1]);
3512         var xNormal1 = (point[1] - previousPoint[1]) / distance1;
3513         var yNormal1 = (previousPoint[0] - point[0]) / distance1;
3514         var distance2 = java.awt.geom.Point2D.distance(
3515             nextPoint[0], nextPoint[1], point[0], point[1]);
3516         var xNormal2 = (nextPoint[1] - point[1]) / distance2;
3517         var yNormal2 = (point[0] - nextPoint[0]) / distance2;
3518         angle = Math.atan2(yNormal1 + yNormal2, xNormal1 + xNormal2);
3519         if (orientateIndicatorOutsideShape 
3520               && item.containsPoint(point[0] + Math.cos(angle), 
3521                   point[1] + Math.sin(angle), 0.001) 
3522             || !orientateIndicatorOutsideShape 
3523                 && (xNormal1 * yNormal2 - yNormal1 * xNormal2) < 0) {
3524           angle += Math.PI;
3525         }
3526       } else if (i === 0) {
3527         angle = angleAtStart;
3528       } else {
3529         angle = angleAtEnd;
3530       }
3531       g2D.rotate(angle);
3532       g2D.draw(resizeIndicator);
3533       g2D.setTransform(previousTransform);
3534     }
3535   }
3536 }
3537 
3538 /**
3539  * Returns the shape of the given indicator type.
3540  * @param {Object} item
3541  * @param {PlanComponent.IndicatorType} indicatorType
3542  * @return {Object}
3543  */
3544 PlanComponent.prototype.getIndicator = function(item, indicatorType) {
3545   if (PlanComponent.IndicatorType.RESIZE === indicatorType) {
3546     if (item instanceof HomePieceOfFurniture) {
3547       return PlanComponent.FURNITURE_RESIZE_INDICATOR;
3548     } else if (item instanceof Compass) {
3549       return PlanComponent.COMPASS_RESIZE_INDICATOR;
3550     } else {
3551       return PlanComponent.WALL_AND_LINE_RESIZE_INDICATOR;
3552     }
3553   } else if (PlanComponent.IndicatorType.ROTATE === indicatorType) {
3554     if (item instanceof HomePieceOfFurniture) {
3555       return PlanComponent.FURNITURE_ROTATION_INDICATOR;
3556     } else if (item instanceof Compass) {
3557       return PlanComponent.COMPASS_ROTATION_INDICATOR;
3558     } else if (item instanceof Camera) {
3559       return PlanComponent.CAMERA_YAW_ROTATION_INDICATOR;
3560     } else if (item instanceof DimensionLine) {
3561       return PlanComponent.DIMENSION_LINE_HEIGHT_ROTATION_INDICATOR;
3562     }
3563   } else if (PlanComponent.IndicatorType.ELEVATE === indicatorType) {
3564     if (item instanceof Camera) {
3565       return PlanComponent.CAMERA_ELEVATION_INDICATOR;
3566     } else {
3567       return PlanComponent.ELEVATION_INDICATOR;
3568     }
3569   } else if (PlanComponent.IndicatorType.RESIZE_HEIGHT === indicatorType) {
3570     if (item instanceof HomePieceOfFurniture) {
3571       return PlanComponent.FURNITURE_HEIGHT_INDICATOR;
3572     } else if (item instanceof DimensionLine) {
3573       return PlanComponent.DIMENSION_LINE_HEIGHT_INDICATOR;
3574     }
3575   } else if (PlanComponent.IndicatorType.CHANGE_POWER === indicatorType) {
3576     if (item instanceof HomeLight) {
3577       return PlanComponent.LIGHT_POWER_INDICATOR;
3578     }
3579   } else if (PlanComponent.IndicatorType.MOVE_TEXT === indicatorType) {
3580     return PlanComponent.TEXT_LOCATION_INDICATOR;
3581   } else if (PlanComponent.IndicatorType.ROTATE_TEXT === indicatorType) {
3582     return PlanComponent.TEXT_ANGLE_INDICATOR;
3583   } else if (PlanComponent.IndicatorType.ROTATE_PITCH === indicatorType) {
3584     if (item instanceof HomePieceOfFurniture) {
3585       return PlanComponent.FURNITURE_PITCH_ROTATION_INDICATOR;
3586     } else if (item instanceof Camera) {
3587       return PlanComponent.CAMERA_PITCH_ROTATION_INDICATOR;
3588     }
3589   } else if (PlanComponent.IndicatorType.ROTATE_ROLL === indicatorType) {
3590     if (item instanceof HomePieceOfFurniture) {
3591       return PlanComponent.FURNITURE_ROLL_ROTATION_INDICATOR;
3592     }
3593   } else if (PlanComponent.IndicatorType.ARC_EXTENT === indicatorType) {
3594     if (item instanceof Wall) {
3595       return PlanComponent.WALL_ARC_EXTENT_INDICATOR;
3596     }
3597   }
3598   return null;
3599 }
3600 
3601 /**
3602  * Paints name indicator on <code>room</code>.
3603  * @param {Graphics2D} g2D
3604  * @param {Room} room
3605  * @param {string|CanvasPattern} indicatorPaint
3606  * @param {number} planScale
3607  * @private
3608  */
3609 PlanComponent.prototype.paintRoomNameOffsetIndicator = function(g2D, room, indicatorPaint, planScale) {
3610   if (this.resizeIndicatorVisible 
3611       && room.getName() != null 
3612       && room.getName().trim().length > 0) {
3613     var xName = room.getXCenter() + room.getNameXOffset();
3614     var yName = room.getYCenter() + room.getNameYOffset();
3615     this.paintTextIndicators(g2D, room, this.getLineCount(room.getName()), 
3616         room.getNameStyle(), xName, yName, room.getNameAngle(), indicatorPaint, planScale);
3617   }
3618 }
3619 
3620 /**
3621  * Paints resize indicator on <code>room</code>.
3622  * @param {Graphics2D} g2D
3623  * @param {Room} room
3624  * @param {string|CanvasPattern} indicatorPaint
3625  * @param {number} planScale
3626  * @private
3627  */
3628 PlanComponent.prototype.paintRoomAreaOffsetIndicator = function(g2D, room, indicatorPaint, planScale) {
3629   if (this.resizeIndicatorVisible 
3630       && room.isAreaVisible() 
3631       && room.getArea() > 0.01) {
3632     var xArea = room.getXCenter() + room.getAreaXOffset();
3633     var yArea = room.getYCenter() + room.getAreaYOffset();
3634     this.paintTextIndicators(g2D, room, 1, room.getAreaStyle(), xArea, yArea, room.getAreaAngle(), 
3635         indicatorPaint, planScale);
3636   }
3637 }
3638 
3639 /**
3640  * Paints text location and angle indicators at the given coordinates.
3641  * @param {Graphics2D} g2D
3642  * @param {Object} selectableObject
3643  * @param {number} lineCount
3644  * @param {TextStyle} style
3645  * @param {number} x
3646  * @param {number} y
3647  * @param {number} angle
3648  * @param {string|CanvasPattern} indicatorPaint
3649  * @param {number} planScale
3650  * @private
3651  */
3652 PlanComponent.prototype.paintTextIndicators = function(g2D, selectableObject, lineCount, style, x, y, angle, indicatorPaint, planScale) {
3653   if (this.resizeIndicatorVisible) {
3654     g2D.setPaint(indicatorPaint);
3655     g2D.setStroke(PlanComponent.INDICATOR_STROKE);
3656     var previousTransform = g2D.getTransform();
3657     var scaleInverse = 1 / planScale;
3658     g2D.translate(x, y);
3659     g2D.rotate(angle);
3660     g2D.scale(scaleInverse, scaleInverse);
3661     if (selectableObject instanceof Label) {
3662       g2D.draw(PlanComponent.LABEL_CENTER_INDICATOR);
3663     } else {
3664       g2D.draw(this.getIndicator(null, PlanComponent.IndicatorType.MOVE_TEXT));
3665     }
3666     if (style == null) {
3667       style = this.preferences.getDefaultTextStyle(selectableObject);
3668     }
3669     var fontMetrics = this.getFontMetrics(g2D.getFont(), style);
3670     g2D.setTransform(previousTransform);
3671     g2D.translate(x, y);
3672     g2D.rotate(angle);
3673     g2D.translate(0, -fontMetrics.getHeight() * (lineCount - 1) 
3674         - fontMetrics.getAscent() * (selectableObject instanceof Label ? 1 : 0.85));
3675     g2D.scale(scaleInverse, scaleInverse);
3676     g2D.draw(this.getIndicator(null, PlanComponent.IndicatorType.ROTATE_TEXT));
3677     g2D.setTransform(previousTransform);
3678   }
3679 }
3680 
3681 /**
3682  * Returns the number of lines in the given <code>text</code> ignoring trailing line returns.
3683  * @param {string} text
3684  * @return {number}
3685  * @private
3686  */
3687 PlanComponent.prototype.getLineCount = function(text) {
3688   var lineCount = 1;
3689   var i = text.length - 1;
3690   while (i >= 0 && text.charAt(i) == '\n') {
3691     i--;
3692   }
3693   for ( ; i >= 0; i--) {
3694     if (text.charAt(i) == '\n') {
3695       lineCount++;
3696     }
3697   }
3698   return lineCount;
3699 }
3700 
3701 /**
3702  * Paints walls.
3703  * @param {Graphics2D} g2D
3704  * @param {Object[]} selectedItems
3705  * @param {Level}  level
3706  * @param {number} planScale
3707  * @param {string} backgroundColor
3708  * @param {string} foregroundColor
3709  * @param {PlanComponent.PaintMode} paintMode
3710  * @private
3711  */
3712 PlanComponent.prototype.paintWalls = function(g2D, selectedItems, level, planScale, backgroundColor, foregroundColor, paintMode) {
3713   var paintedWalls;
3714   var wallAreas;
3715   if (paintMode !== PlanComponent.PaintMode.CLIPBOARD) {
3716     wallAreas = this.getWallAreasAtLevel(level);
3717   } else {
3718     paintedWalls = Home.getWallsSubList(selectedItems);
3719     wallAreas = this.getWallAreas(this.getDrawableWallsAtLevel(paintedWalls, level));
3720   }
3721   var wallPaintScale = paintMode === PlanComponent.PaintMode.PRINT 
3722       ? planScale / 72 * 150 
3723       : planScale;
3724   var oldComposite = null;
3725   if (paintMode === PlanComponent.PaintMode.PAINT 
3726       && this.backgroundPainted 
3727       && this.backgroundImageCache != null 
3728       && this.wallsDoorsOrWindowsModification) {
3729     oldComposite = this.setTransparency(g2D, 0.5);
3730   }
3731   
3732   var areaEntries = wallAreas.entries == null ? [] : wallAreas.entries; // Parse entrySet
3733   for (var i = 0; i < areaEntries.length; i++) {
3734     var areaEntry = areaEntries[i];
3735     var wallPattern = areaEntry.getKey() [0].getPattern();
3736     this.fillAndDrawWallsArea(g2D, areaEntry.getValue(), planScale, 
3737         this.getWallPaint(g2D, wallPaintScale, backgroundColor, foregroundColor, 
3738             wallPattern != null ? wallPattern : this.preferences.getWallPattern()), foregroundColor, paintMode);
3739   }
3740   if (oldComposite != null) {
3741     g2D.setAlpha(oldComposite);
3742   }
3743 }
3744 
3745 /**
3746  * Fills and paints the given area.
3747  * @param {Graphics2D} g2D
3748  * @param {java.awt.geom.Area} area
3749  * @param {number} planScale
3750  * @param {string|CanvasPattern} fillPaint
3751  * @param {string|CanvasPattern} drawPaint
3752  * @param {PlanComponent.PaintMode} paintMode
3753  * @private
3754  */
3755 PlanComponent.prototype.fillAndDrawWallsArea = function(g2D, area, planScale, fillPaint, drawPaint, paintMode) {
3756   g2D.setPaint(fillPaint);
3757   var patternScale = 1 / planScale;
3758   g2D.scale(patternScale, patternScale);
3759   var filledArea = area.clone();
3760   filledArea.transform(java.awt.geom.AffineTransform.getScaleInstance(1 / patternScale, 1 / patternScale));
3761   this.fillShape(g2D, filledArea, paintMode);
3762   g2D.scale(1 / patternScale, 1 / patternScale);
3763   g2D.setPaint(drawPaint);
3764   g2D.setStroke(new java.awt.BasicStroke(this.getStrokeWidth(Wall, paintMode) / planScale));
3765   g2D.draw(area);
3766 }
3767 
3768 /**
3769  * Paints the outline of walls among <code>items</code> and a resize indicator if
3770  * <code>items</code> contains only one wall and indicator paint isn't <code>null</code>.
3771  * @param {Graphics2D} g2D
3772  * @param {Object[]} items
3773  * @param {Level} level
3774  * @param {string|CanvasPattern} selectionOutlinePaint
3775  * @param {java.awt.BasicStroke} selectionOutlineStroke
3776  * @param {string|CanvasPattern} indicatorPaint
3777  * @param {number} planScale
3778  * @param {string} foregroundColor
3779  * @private
3780  */
3781 PlanComponent.prototype.paintWallsOutline = function(g2D, items, level, selectionOutlinePaint, selectionOutlineStroke, indicatorPaint, planScale, foregroundColor) {
3782   var scaleInverse = 1 / planScale;
3783   var walls = Home.getWallsSubList(items);
3784   var previousTransform = g2D.getTransform();
3785   for (var i = 0; i < walls.length; i++) {
3786     var wall = walls[i];
3787     if (this.isViewableAtLevel(wall, level)) {
3788       g2D.setPaint(selectionOutlinePaint);
3789       g2D.setStroke(selectionOutlineStroke);
3790       g2D.draw(ShapeTools.getShape(wall.getPoints(), true, null));
3791       
3792       if (indicatorPaint != null) {
3793         g2D.translate(wall.getXStart(), wall.getYStart());
3794         g2D.scale(scaleInverse, scaleInverse);
3795         g2D.setPaint(indicatorPaint);
3796         g2D.setStroke(PlanComponent.POINT_STROKE);
3797         g2D.fill(PlanComponent.WALL_POINT);
3798         
3799         var arcExtent = wall.getArcExtent();
3800         var indicatorAngle = void 0;
3801         var distanceAtScale = void 0;
3802         var xArcCircleCenter = 0;
3803         var yArcCircleCenter = 0;
3804         var arcCircleRadius = 0;
3805         var startPointToEndPointDistance = wall.getStartPointToEndPointDistance();
3806         var wallAngle = Math.atan2(wall.getYEnd() - wall.getYStart(), 
3807             wall.getXEnd() - wall.getXStart());
3808         if (arcExtent != null && arcExtent !== 0) {
3809           xArcCircleCenter = wall.getXArcCircleCenter();
3810           yArcCircleCenter = wall.getYArcCircleCenter();
3811           arcCircleRadius = java.awt.geom.Point2D.distance(wall.getXStart(), wall.getYStart(), 
3812               xArcCircleCenter, yArcCircleCenter);
3813           distanceAtScale = arcCircleRadius * Math.abs(arcExtent) * planScale;
3814           indicatorAngle = Math.atan2(yArcCircleCenter - wall.getYStart(), 
3815                   xArcCircleCenter - wall.getXStart()) 
3816               + (arcExtent > 0 ? -Math.PI / 2 : Math.PI / 2);
3817         } else {
3818           distanceAtScale = startPointToEndPointDistance * planScale;
3819           indicatorAngle = wallAngle;
3820         }
3821         if (distanceAtScale < 30) {
3822           g2D.rotate(wallAngle);
3823           if (arcExtent != null) {
3824             var wallToStartPointArcCircleCenterAngle = Math.abs(arcExtent) > Math.PI 
3825                 ? -(Math.PI + arcExtent) / 2 
3826                 : (Math.PI - arcExtent) / 2;
3827             var arcCircleCenterToWallDistance = (Math.tan(wallToStartPointArcCircleCenterAngle) 
3828                 * startPointToEndPointDistance / 2);
3829             g2D.translate(startPointToEndPointDistance * planScale / 2, 
3830                 (arcCircleCenterToWallDistance - arcCircleRadius * (Math.abs(wallAngle) > Math.PI / 2 ? -1 : 1)) * planScale);
3831           } else {
3832             g2D.translate(distanceAtScale / 2, 0);
3833           }
3834         } else {
3835           g2D.rotate(indicatorAngle);
3836           g2D.translate(8, 0);
3837         }
3838         g2D.draw(PlanComponent.WALL_ORIENTATION_INDICATOR);
3839         g2D.setTransform(previousTransform);
3840         g2D.translate(wall.getXEnd(), wall.getYEnd());
3841         g2D.scale(scaleInverse, scaleInverse);
3842         g2D.fill(PlanComponent.WALL_POINT);
3843         if (distanceAtScale >= 30) {
3844           if (arcExtent != null) {
3845             indicatorAngle += arcExtent;
3846           }
3847           g2D.rotate(indicatorAngle);
3848           g2D.translate(-10, 0);
3849           g2D.draw(PlanComponent.WALL_ORIENTATION_INDICATOR);
3850         }
3851         g2D.setTransform(previousTransform);
3852       }
3853     }
3854   }
3855   g2D.setPaint(foregroundColor);
3856   g2D.setStroke(new java.awt.BasicStroke(this.getStrokeWidth(Wall, PlanComponent.PaintMode.PAINT) / planScale));
3857   var areas = CoreTools.valuesFromMap(this.getWallAreas(this.getDrawableWallsAtLevel(walls, level)));
3858   for (var i = 0; i < areas.length; i++) {
3859     g2D.draw(areas[i]);
3860   }
3861   if (items.length === 1 
3862       && walls.length === 1 
3863       && indicatorPaint != null) {
3864     var wall = walls[0];
3865     if (this.isViewableAtLevel(wall, level)) {
3866       this.paintWallResizeIndicators(g2D, wall, indicatorPaint, planScale);
3867     }
3868   }
3869 }
3870 
3871 /**
3872  * Returns <code>true</code> if the given item can be viewed in the plan at a level.
3873  * @param {Object} item
3874  * @param {Level} level
3875  * @return {boolean}
3876  */
3877 PlanComponent.prototype.isViewableAtLevel = function(item, level) {
3878   var itemLevel = item.getLevel();
3879   return itemLevel == null
3880       || (itemLevel.isViewable()
3881           && item.isAtLevel(level));
3882 }
3883   
3884 /**
3885  * Returns <code>true</code> if the given item can be viewed in the plan at the selected level.
3886  * @deprecated Override {@link #isViewableAtLevel} if you want to print different levels
3887  * @param {Object} item
3888  * @return {boolean}
3889  */
3890 PlanComponent.prototype.isViewableAtSelectedLevel = function(item) {
3891   return this.isViewableAtLevel(item, this.home.getSelectedLevel());
3892 }
3893 
3894 /**
3895  * Paints resize indicators on <code>wall</code>.
3896  * @param {Graphics2D} g2D
3897  * @param {Wall} wall
3898  * @param {string|CanvasPattern} indicatorPaint
3899  * @param {number} planScale
3900  * @private
3901  */
3902 PlanComponent.prototype.paintWallResizeIndicators = function(g2D, wall, indicatorPaint, planScale) {
3903   if (this.resizeIndicatorVisible) {
3904     g2D.setPaint(indicatorPaint);
3905     g2D.setStroke(PlanComponent.INDICATOR_STROKE);
3906     var previousTransform = g2D.getTransform();
3907     var scaleInverse = 1 / planScale;
3908     var wallPoints = wall.getPoints();
3909     var leftSideMiddlePointIndex = (wallPoints.length / 4 | 0);
3910     var wallAngle = Math.atan2(wall.getYEnd() - wall.getYStart(), 
3911         wall.getXEnd() - wall.getXStart());
3912     
3913     if (wallPoints.length % 4 === 0) {
3914       g2D.translate((wallPoints[leftSideMiddlePointIndex - 1][0] + wallPoints[leftSideMiddlePointIndex][0]) / 2, 
3915           (wallPoints[leftSideMiddlePointIndex - 1][1] + wallPoints[leftSideMiddlePointIndex][1]) / 2);
3916     } else {
3917       g2D.translate(wallPoints[leftSideMiddlePointIndex][0], wallPoints[leftSideMiddlePointIndex][1]);
3918     }
3919     g2D.scale(scaleInverse, scaleInverse);
3920     g2D.rotate(wallAngle + Math.PI);
3921     g2D.draw(this.getIndicator(wall, PlanComponent.IndicatorType.ARC_EXTENT));
3922     g2D.setTransform(previousTransform);
3923     
3924     var arcExtent = wall.getArcExtent();
3925     var indicatorAngle = void 0;
3926     if (arcExtent != null && arcExtent !== 0) {
3927       indicatorAngle = Math.atan2(wall.getYArcCircleCenter() - wall.getYEnd(), 
3928               wall.getXArcCircleCenter() - wall.getXEnd()) 
3929           + (arcExtent > 0 ? -Math.PI / 2 : Math.PI / 2);
3930     } else {
3931       indicatorAngle = wallAngle;
3932     }
3933     
3934     g2D.translate(wall.getXEnd(), wall.getYEnd());
3935     g2D.scale(scaleInverse, scaleInverse);
3936     g2D.rotate(indicatorAngle);
3937     g2D.draw(this.getIndicator(wall, PlanComponent.IndicatorType.RESIZE));
3938     g2D.setTransform(previousTransform);
3939     
3940     if (arcExtent != null) {
3941       indicatorAngle += Math.PI - arcExtent;
3942     } else {
3943       indicatorAngle += Math.PI;
3944     }
3945     
3946     g2D.translate(wall.getXStart(), wall.getYStart());
3947     g2D.scale(scaleInverse, scaleInverse);
3948     g2D.rotate(indicatorAngle);
3949     g2D.draw(this.getIndicator(wall, PlanComponent.IndicatorType.RESIZE));
3950     g2D.setTransform(previousTransform);
3951   }
3952 }
3953 
3954 /**
3955  * Returns areas matching the union of home wall shapes sorted by pattern.
3956  * @param {Level} level
3957  * @return {Object}
3958  * @private
3959  */
3960 PlanComponent.prototype.getWallAreasAtLevel = function(level) {
3961   if (this.wallAreasCache == null) {
3962     this.wallAreasCache = this.getWallAreas(this.getDrawableWallsAtLevel(this.home.getWalls(), level));
3963   }
3964   return this.wallAreasCache;
3965 }
3966 
3967 /**
3968  * Returns the walls that belong to the given <code>level</code> in home.
3969  * @param {Wall[]} walls
3970  * @param {Level} level
3971  * @return {Wall[]}
3972  * @private
3973  */
3974 PlanComponent.prototype.getDrawableWallsAtLevel = function(walls, level) {
3975   var wallsAtLevel = [];
3976   for (var i = 0; i < walls.length; i++) {
3977     var wall = walls[i];
3978     if (this.isViewableAtLevel(wall, level)) {
3979       wallsAtLevel.push(wall);
3980     }
3981   }
3982   return wallsAtLevel;
3983 }
3984 
3985 /**
3986  * Returns areas matching the union of <code>walls</code> shapes sorted by pattern.
3987  * @param {Wall[]} walls
3988  * @return {Object} Map<Collection<Wall>, Area>
3989  * @private
3990  */
3991 PlanComponent.prototype.getWallAreas = function(walls) {
3992   var plan = this;
3993   if (walls.length === 0) {
3994     return {};
3995   }
3996   var pattern = walls[0].getPattern();
3997   var samePattern = true;
3998   for (var i = 0; i < walls.length; i++) {
3999     if (pattern !== walls[i].getPattern()) {
4000       samePattern = false;
4001       break;
4002     }
4003   }
4004   var wallAreas = {};
4005   if (samePattern) {
4006     CoreTools.putToMap(wallAreas, walls, this.getItemsArea(walls));
4007   } else {
4008     var sortedWalls = {}; // LinkedHashMap
4009     walls.forEach(function(wall) {
4010         var wallPattern = wall.getPattern();
4011         if (wallPattern == null) {
4012           wallPattern = plan.preferences.getWallPattern();
4013         }
4014         var patternWalls = CoreTools.getFromMap(sortedWalls, wallPattern);
4015         if (patternWalls == null) {
4016           patternWalls = [];
4017           CoreTools.putToMap(sortedWalls, wallPattern, patternWalls);
4018         }
4019         patternWalls.push(wall);
4020       });
4021     
4022     var walls = CoreTools.valuesFromMap(sortedWalls);
4023     for (var i = 0; i < walls.length; i++) {
4024       var patternWalls = walls[i];
4025       CoreTools.putToMap(wallAreas, patternWalls, this.getItemsArea(patternWalls));
4026     }
4027   }
4028   return wallAreas;
4029 }
4030 
4031 /**
4032  * Returns an area matching the union of all <code>items</code> shapes.
4033  * @param {Bound[]} items
4034  * @return {java.awt.geom.Area}
4035  * @private
4036  */
4037 PlanComponent.prototype.getItemsArea = function(items) {
4038   var itemsArea = new java.awt.geom.Area();
4039   items.forEach(function(item) { 
4040       itemsArea.add(new java.awt.geom.Area(ShapeTools.getShape(item.getPoints(), true, null))); 
4041     });
4042   return itemsArea;
4043 }
4044 
4045 /**
4046  * Modifies the pattern image to substitute the transparent color with backgroundColor and the black color with the foregroundColor.
4047  * @param {HTMLImageElement} image the orginal pattern image (black over transparent background)
4048  * @param {string} foregroundColor the foreground color
4049  * @param {string} backgroundColor the background color
4050  * @return {HTMLImageElement} the final pattern image with the substituted colors
4051  * @private
4052  */
4053 PlanComponent.prototype.makePatternImage = function(image, foregroundColor, backgroundColor) {
4054   var canvas = document.createElement("canvas");
4055   canvas.width = image.naturalWidth;
4056   canvas.height = image.naturalHeight;
4057   var context = canvas.getContext("2d");
4058   context.fillStyle = "#FFFFFF";
4059   context.fillRect(0, 0, canvas.width, canvas.height);
4060   context.drawImage(image, 0, 0);
4061   var imageData = context.getImageData(0, 0, image.naturalWidth, image.height).data;
4062   var bgColor = ColorTools.hexadecimalStringToInteger(backgroundColor);
4063   var fgColor = ColorTools.hexadecimalStringToInteger(foregroundColor);
4064   var updatedImageData = context.createImageData(image.naturalWidth, image.height);
4065   for (var i = 0; i < imageData.length; i += 4) {
4066     updatedImageData.data[i + 3] = 0xFF;
4067     if (imageData[i] === 0xFF && imageData[i + 1] === 0xFF && imageData[i + 2] === 0xFF) {
4068       // Change white pixels to background color
4069       updatedImageData.data[i] = (bgColor & 0xFF0000) >> 16;
4070       updatedImageData.data[i + 1] = (bgColor & 0xFF00) >> 8;
4071       updatedImageData.data[i + 2] = bgColor & 0xFF;
4072     } else if (imageData[i] === 0 && imageData[i + 1] === 0 && imageData[i + 2] === 0) {
4073       // Change black pixels to foreground color
4074       updatedImageData.data[i] = (fgColor & 0xFF0000) >> 16;
4075       updatedImageData.data[i + 1] = (fgColor & 0xFF00) >> 8;
4076       updatedImageData.data[i + 2] = fgColor & 0xFF;
4077     } else {
4078       // Change color mixing foreground and background color 
4079       var percent = 1 - (imageData[i] + imageData[i + 1] + imageData[i + 2]) / 3. / 0xFF;
4080       updatedImageData.data[i] = Math.min(0xFF, ((fgColor & 0xFF0000) >> 16) * percent + ((bgColor & 0xFF0000) >> 16) * (1 - percent));
4081       updatedImageData.data[i + 1] = Math.min(0xFF, ((fgColor & 0xFF00) >> 8) * percent + ((bgColor & 0xFF00) >> 8) * (1 - percent));
4082       updatedImageData.data[i + 2] = Math.min(0xFF, (fgColor & 0xFF) * percent + (bgColor & 0xFF) * (1 - percent));
4083     }
4084   }
4085   context.putImageData(updatedImageData, 0, 0);
4086   image.src = canvas.toDataURL();
4087   return image;
4088 }
4089 
4090 /**
4091  * Returns the <code>Paint</code> object used to fill walls.
4092  * @param {Graphics2D} g2D
4093  * @param {number} planScale
4094  * @param {string} backgroundColor
4095  * @param {string} foregroundColor
4096  * @param {TextureImage} wallPattern
4097  * @return {Object}
4098  * @private
4099  */
4100 PlanComponent.prototype.getWallPaint = function(g2D, planScale, backgroundColor, foregroundColor, wallPattern) {
4101   var plan = this;
4102   var patternImage = this.patternImagesCache[wallPattern.getImage().getURL()];
4103   if (patternImage == null 
4104       || backgroundColor != this.wallsPatternBackgroundCache 
4105       || foregroundColor != this.wallsPatternForegroundCache) {
4106     patternImage = TextureManager.getInstance().getWaitImage();
4107     this.patternImagesCache[wallPattern.getImage().getURL()] = patternImage;
4108     TextureManager.getInstance().loadTexture(wallPattern.getImage(), false, {
4109         textureUpdated: function(image) {
4110           plan.patternImagesCache[wallPattern.getImage().getURL()] = plan.makePatternImage(image, plan.getForeground(), plan.getBackground());
4111           plan.repaint();
4112         },
4113         textureError: function() {
4114           plan.patternImagesCache[wallPattern.getImage().getURL()] = PlanComponent.ERROR_TEXTURE_IMAGE;
4115         },
4116         progression: function() { }
4117       });
4118     this.wallsPatternBackgroundCache = backgroundColor;
4119     this.wallsPatternForegroundCache = foregroundColor;
4120   }
4121   return g2D.createPattern(patternImage);
4122 }
4123 
4124 /**
4125  * Paints home furniture.
4126  * @param {Graphics2D} g2D
4127  * @param {HomePieceOfFurniture[]} furniture
4128  * @param {Bound[]} selectedItems
4129  * @param {Level}  level 
4130  * @param {number} planScale
4131  * @param {string} backgroundColor
4132  * @param {string} foregroundColor
4133  * @param {string} furnitureOutlineColor
4134  * @param {PlanComponent.PaintMode} paintMode
4135  * @param {boolean} paintIcon
4136  * @private
4137  */
4138 PlanComponent.prototype.paintFurniture = function(g2D, furniture, selectedItems, level, planScale, backgroundColor, foregroundColor, furnitureOutlineColor, paintMode, paintIcon) {
4139   if (!(furniture.length == 0)) {
4140     var pieceBorderStroke = new java.awt.BasicStroke(this.getStrokeWidth(HomePieceOfFurniture, paintMode) / planScale);
4141     var allFurnitureViewedFromTop = null;
4142     for (var i = 0; i < furniture.length; i++) {
4143       var piece = furniture[i];
4144       if (piece.isVisible()) {
4145         var selectedPiece = (selectedItems.indexOf((piece)) >= 0);
4146         if (piece instanceof HomeFurnitureGroup) {
4147           var groupFurniture = (piece).getFurniture();
4148           var emptyList = [];
4149           this.paintFurniture(g2D, groupFurniture, 
4150               selectedPiece 
4151                   ? groupFurniture 
4152                   : emptyList, level,
4153               planScale, backgroundColor, foregroundColor, 
4154               furnitureOutlineColor, paintMode, paintIcon);
4155         } else if (paintMode !== PlanComponent.PaintMode.CLIPBOARD || selectedPiece) {
4156           var pieceShape = ShapeTools.getShape(piece.getPoints(), true, null);
4157           var pieceShape2D = void 0;
4158           if (piece instanceof HomeDoorOrWindow) {
4159             var doorOrWindow = piece;
4160             pieceShape2D = this.getDoorOrWindowWallPartShape(doorOrWindow);
4161             if (this.draggedItemsFeedback == null 
4162                 || !(this.draggedItemsFeedback.indexOf((piece)) >= 0)) {
4163               this.paintDoorOrWindowWallThicknessArea(g2D, doorOrWindow, planScale, backgroundColor, foregroundColor, paintMode);
4164             }
4165             this.paintDoorOrWindowSashes(g2D, doorOrWindow, planScale, foregroundColor, paintMode);
4166           } else {
4167             pieceShape2D = pieceShape;
4168           }
4169           
4170           var viewedFromTop = void 0;
4171           if (this.preferences.isFurnitureViewedFromTop()) {
4172             if (piece.getPlanIcon() != null 
4173                 || (piece instanceof HomeDoorOrWindow)) {
4174               viewedFromTop = true;
4175             } else {
4176               allFurnitureViewedFromTop = PlanComponent.WEBGL_AVAILABLE;
4177               viewedFromTop = allFurnitureViewedFromTop;
4178             }
4179           } else {
4180             viewedFromTop = false;
4181           }
4182           if (paintIcon && viewedFromTop) {
4183             if (piece instanceof HomeDoorOrWindow) {
4184               g2D.setPaint(backgroundColor);
4185               g2D.fill(pieceShape2D);
4186               g2D.setPaint(foregroundColor);
4187               g2D.setStroke(pieceBorderStroke);
4188               g2D.draw(pieceShape2D);
4189             } else {
4190               this.paintPieceOfFurnitureTop(g2D, piece, pieceShape2D, pieceBorderStroke, planScale, 
4191                   backgroundColor, foregroundColor, paintMode);
4192             }
4193             if (paintMode === PlanComponent.PaintMode.PAINT) {
4194               g2D.setStroke(pieceBorderStroke);
4195               g2D.setPaint(furnitureOutlineColor);
4196               g2D.draw(pieceShape);
4197             }
4198           } else {
4199             if (paintIcon) {
4200               this.paintPieceOfFurnitureIcon(g2D, piece, null, pieceShape2D, planScale, 
4201                   backgroundColor, paintMode);
4202             }
4203             g2D.setPaint(foregroundColor);
4204             g2D.setStroke(pieceBorderStroke);
4205             g2D.draw(pieceShape2D);
4206             if ((piece instanceof HomeDoorOrWindow) 
4207                 && paintMode === PlanComponent.PaintMode.PAINT) {
4208               g2D.setPaint(furnitureOutlineColor);
4209               g2D.draw(pieceShape);
4210             }
4211           }
4212         }
4213       }
4214     }
4215   }
4216 }
4217 
4218 /**
4219  * Returns the shape of the wall part of a door or a window.
4220  * @param {HomeDoorOrWindow} doorOrWindow
4221  * @return {Object}
4222  * @private
4223  */
4224 PlanComponent.prototype.getDoorOrWindowWallPartShape = function(doorOrWindow) {
4225   var doorOrWindowWallPartRectangle = this.getDoorOrWindowRectangle(doorOrWindow, true);
4226   var rotation = java.awt.geom.AffineTransform.getRotateInstance(
4227       doorOrWindow.getAngle(), doorOrWindow.getX(), doorOrWindow.getY());
4228   var it = doorOrWindowWallPartRectangle.getPathIterator(rotation);
4229   var doorOrWindowWallPartShape = new java.awt.geom.GeneralPath();
4230   doorOrWindowWallPartShape.append(it, false);
4231   return doorOrWindowWallPartShape;
4232 }
4233 
4234 /**
4235  * Returns the rectangle of a door or a window.
4236  * @param {HomeDoorOrWindow} doorOrWindow
4237  * @param {boolean} onlyWallPart
4238  * @return {java.awt.geom.Rectangle2D}
4239  * @private
4240  */
4241 PlanComponent.prototype.getDoorOrWindowRectangle = function(doorOrWindow, onlyWallPart) {
4242   var wallThickness = doorOrWindow.getDepth() * (onlyWallPart ? doorOrWindow.getWallThickness() : 1);
4243   var wallDistance = doorOrWindow.getDepth() * (onlyWallPart ? doorOrWindow.getWallDistance() : 0);
4244   var cutOutShape = doorOrWindow.getCutOutShape();
4245   var width = doorOrWindow.getWidth();
4246   var wallWidth = doorOrWindow.getWallWidth() * width;
4247   var x = doorOrWindow.getX() - width / 2;
4248   x += doorOrWindow.isModelMirrored() 
4249       ? (1 - doorOrWindow.getWallLeft() - doorOrWindow.getWallWidth()) * width 
4250       : doorOrWindow.getWallLeft() * width;
4251   if (cutOutShape != null 
4252       && PieceOfFurniture.DEFAULT_CUT_OUT_SHAPE != cutOutShape) {
4253     var shape = ShapeTools.getShape(cutOutShape);
4254     var bounds = shape.getBounds2D();
4255     if (doorOrWindow.isModelMirrored()) {
4256       x += (1 - bounds.getX() - bounds.getWidth()) * wallWidth;
4257     } else {
4258       x += bounds.getX() * wallWidth;
4259     }
4260     wallWidth *= bounds.getWidth();
4261   }
4262   var doorOrWindowWallPartRectangle = new java.awt.geom.Rectangle2D.Float(
4263       x, doorOrWindow.getY() - doorOrWindow.getDepth() / 2 + wallDistance, 
4264       wallWidth, wallThickness);
4265   return doorOrWindowWallPartRectangle;
4266 }
4267 
4268 /**
4269  * Paints the shape of a door or a window in the thickness of the wall it intersects.
4270  * @param {Graphics2D} g2D
4271  * @param {HomeDoorOrWindow} doorOrWindow
4272  * @param {number} planScale
4273  * @param {string} backgroundColor
4274  * @param {string} foregroundColor
4275  * @param {PlanComponent.PaintMode} paintMode
4276  * @private
4277  */
4278 PlanComponent.prototype.paintDoorOrWindowWallThicknessArea = function(g2D, doorOrWindow, planScale, backgroundColor, foregroundColor, paintMode) {
4279   if (doorOrWindow.isWallCutOutOnBothSides()) {
4280     var doorOrWindowWallArea = null;
4281     if (this.doorOrWindowWallThicknessAreasCache != null) {
4282       doorOrWindowWallArea = CoreTools.getFromMap(this.doorOrWindowWallThicknessAreasCache, doorOrWindow);
4283     }
4284     
4285     if (doorOrWindowWallArea == null) {
4286       var doorOrWindowRectangle = this.getDoorOrWindowRectangle(doorOrWindow, false);
4287       var rotation = java.awt.geom.AffineTransform.getRotateInstance(
4288           doorOrWindow.getAngle(), doorOrWindow.getX(), doorOrWindow.getY());
4289       var it = doorOrWindowRectangle.getPathIterator(rotation);
4290       var doorOrWindowWallPartShape = new java.awt.geom.GeneralPath();
4291       doorOrWindowWallPartShape.append(it, false);
4292       var doorOrWindowWallPartArea = new java.awt.geom.Area(doorOrWindowWallPartShape);
4293       
4294       doorOrWindowWallArea = new java.awt.geom.Area();
4295       var walls = this.home.getWalls();
4296       for (var i = 0; i < walls.length; i++) {
4297         var wall = walls[i];
4298         if (wall.isAtLevel(doorOrWindow.getLevel()) 
4299             && doorOrWindow.isParallelToWall(wall)) {
4300           var wallShape = ShapeTools.getShape(wall.getPoints(), true, null);
4301           var wallArea = new java.awt.geom.Area(wallShape);
4302           wallArea.intersect(doorOrWindowWallPartArea);
4303           if (!wallArea.isEmpty()) {
4304             var doorOrWindowExtendedRectangle = new java.awt.geom.Rectangle2D.Float(
4305                 doorOrWindowRectangle.getX(), 
4306                 doorOrWindowRectangle.getY() - 2 * wall.getThickness(), 
4307                 doorOrWindowRectangle.getWidth(), 
4308                 doorOrWindowRectangle.getWidth() + 4 * wall.getThickness());
4309             it = doorOrWindowExtendedRectangle.getPathIterator(rotation);
4310             var path = new java.awt.geom.GeneralPath();
4311             path.append(it, false);
4312             wallArea = new java.awt.geom.Area(wallShape);
4313             wallArea.intersect(new java.awt.geom.Area(path));
4314             doorOrWindowWallArea.add(wallArea);
4315           }
4316         }
4317       }
4318     }
4319     
4320     if (this.doorOrWindowWallThicknessAreasCache == null) {
4321       this.doorOrWindowWallThicknessAreasCache = {};
4322     }
4323     CoreTools.putToMap(this.doorOrWindowWallThicknessAreasCache, doorOrWindow, doorOrWindowWallArea);
4324     
4325     g2D.setPaint(backgroundColor);
4326     g2D.fill(doorOrWindowWallArea);
4327     g2D.setPaint(foregroundColor);
4328     g2D.setStroke(new java.awt.BasicStroke(this.getStrokeWidth(HomePieceOfFurniture, paintMode) / planScale));
4329     g2D.draw(doorOrWindowWallArea);
4330   }
4331 }
4332 
4333 /**
4334  * Paints the sashes of a door or a window.
4335  * @param {Graphics2D} g2D
4336  * @param {HomeDoorOrWindow} doorOrWindow
4337  * @param {number} planScale
4338  * @param {string} foregroundColor
4339  * @param {PlanComponent.PaintMode} paintMode
4340  * @private
4341  */
4342 PlanComponent.prototype.paintDoorOrWindowSashes = function(g2D, doorOrWindow, planScale, foregroundColor, paintMode) {
4343   var sashBorderStroke = new java.awt.BasicStroke(this.getStrokeWidth(HomePieceOfFurniture, paintMode) / planScale);
4344   g2D.setPaint(foregroundColor);
4345   g2D.setStroke(sashBorderStroke);
4346   var sashes = doorOrWindow.getSashes();
4347   for (var i = 0; i < sashes.length; i++) {
4348     g2D.draw(this.getDoorOrWindowSashShape(doorOrWindow, sashes[i]));
4349   }
4350 }
4351 
4352 /**
4353  * Returns the shape of a sash of a door or a window.
4354  * @param {HomeDoorOrWindow} doorOrWindow
4355  * @param {Sash} sash
4356  * @return {java.awt.geom.GeneralPath}
4357  * @private
4358  */
4359 PlanComponent.prototype.getDoorOrWindowSashShape = function(doorOrWindow, sash) {
4360   var modelMirroredSign = doorOrWindow.isModelMirrored() ? -1 : 1;
4361   var xAxis = modelMirroredSign * sash.getXAxis() * doorOrWindow.getWidth();
4362   var yAxis = sash.getYAxis() * doorOrWindow.getDepth();
4363   var sashWidth = sash.getWidth() * doorOrWindow.getWidth();
4364   var startAngle = sash.getStartAngle() * 180 / Math.PI;
4365   if (doorOrWindow.isModelMirrored()) {
4366     startAngle = 180 - startAngle;
4367   }
4368   var extentAngle = modelMirroredSign * ((sash.getEndAngle() - sash.getStartAngle()) * 180 / Math.PI);
4369   
4370   var arc = new java.awt.geom.Arc2D.Float(xAxis - sashWidth, yAxis - sashWidth, 
4371       2 * sashWidth, 2 * sashWidth, 
4372       startAngle, extentAngle, java.awt.geom.Arc2D.PIE);
4373   var transformation = java.awt.geom.AffineTransform.getTranslateInstance(doorOrWindow.getX(), doorOrWindow.getY());
4374   transformation.rotate(doorOrWindow.getAngle());
4375   transformation.translate(modelMirroredSign * -doorOrWindow.getWidth() / 2, -doorOrWindow.getDepth() / 2);
4376   var it = arc.getPathIterator(transformation);
4377   var sashShape = new java.awt.geom.GeneralPath();
4378   sashShape.append(it, false);
4379   return sashShape;
4380 }
4381 
4382 /**
4383  * Paints home furniture visible name.
4384  * @param {Graphics2D} g2D
4385  * @param {HomePieceOfFurniture[]} furniture
4386  * @param {Bound[]} selectedItems
4387  * @param {number} planScale
4388  * @param {string} foregroundColor
4389  * @param {PlanComponent.PaintMode} paintMode
4390  * @private
4391  */
4392 PlanComponent.prototype.paintFurnitureName = function(g2D, furniture, selectedItems, planScale, foregroundColor, paintMode) {
4393   var previousFont = g2D.getFont();
4394   g2D.setPaint(foregroundColor);
4395   for (var i = 0; i < furniture.length; i++) {
4396     var piece = furniture[i];
4397     if (piece.isVisible()) {
4398       var selectedPiece = (selectedItems.indexOf((piece)) >= 0);
4399       if (piece instanceof HomeFurnitureGroup) {
4400         var groupFurniture = (piece).getFurniture();
4401         var emptyList = [];
4402         this.paintFurnitureName(g2D, groupFurniture, 
4403             selectedPiece 
4404                 ? groupFurniture 
4405                 : emptyList, 
4406             planScale, foregroundColor, paintMode);
4407       }
4408       if (piece.isNameVisible() 
4409           && (paintMode !== PlanComponent.PaintMode.CLIPBOARD 
4410               || selectedPiece)) {
4411         var name = piece.getName().trim();
4412         if (name.length > 0) {
4413           this.paintText(g2D, piece.constructor, name, piece.getNameStyle(), null, 
4414               piece.getX() + piece.getNameXOffset(), 
4415               piece.getY() + piece.getNameYOffset(), 
4416               piece.getNameAngle(), previousFont);
4417         }
4418       }
4419     }
4420   }
4421   g2D.setFont(previousFont);
4422 }
4423 
4424 /**
4425  * Paints the outline of furniture among <code>items</code> and indicators if
4426  * <code>items</code> contains only one piece and indicator paint isn't <code>null</code>.
4427  * @param {Graphics2D} g2D
4428  * @param {Object[]} items
4429  * @param {Level} level 
4430  * @param {string|CanvasPattern} selectionOutlinePaint
4431  * @param {java.awt.BasicStroke} selectionOutlineStroke
4432  * @param {string|CanvasPattern} indicatorPaint
4433  * @param {number} planScale
4434  * @param {string} foregroundColor
4435  * @private
4436  */
4437 PlanComponent.prototype.paintFurnitureOutline = function(g2D, items, level, selectionOutlinePaint, selectionOutlineStroke, indicatorPaint, planScale, foregroundColor) {
4438   var plan = this;
4439   var pieceBorderStroke = new java.awt.BasicStroke(this.getStrokeWidth(HomePieceOfFurniture, PlanComponent.PaintMode.PAINT) / planScale);
4440   var pieceFrontBorderStroke = new java.awt.BasicStroke(4 * this.getStrokeWidth(HomePieceOfFurniture, PlanComponent.PaintMode.PAINT) / planScale, 
4441       java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_MITER);
4442   
4443   var furniture = Home.getFurnitureSubList(items);
4444   var paintedFurniture = [];
4445   var furnitureGroupsArea = null;
4446   var furnitureGroupsStroke = new java.awt.BasicStroke(15 / planScale, java.awt.BasicStroke.CAP_SQUARE, java.awt.BasicStroke.JOIN_ROUND);
4447   var lastGroup = null;
4448   var furnitureInGroupsArea = null;
4449   var homeFurniture = this.home.getFurniture();
4450   for (var i = 0; i < furniture.length; i++) {
4451     var piece = furniture [i];
4452     if (piece.isVisible() 
4453         && this.isViewableAtLevel(piece, level)) {
4454       var homePieceOfFurniture = this.getPieceOfFurnitureInHomeFurniture(piece, homeFurniture);
4455       if (homePieceOfFurniture !== piece) {
4456         var groupArea = null;
4457         if (lastGroup !== homePieceOfFurniture) {
4458           var groupShape = ShapeTools.getShape(homePieceOfFurniture.getPoints(), true, null);
4459           groupArea = new java.awt.geom.Area(groupShape);
4460           groupArea.add(new java.awt.geom.Area(furnitureGroupsStroke.createStrokedShape(groupShape)));
4461         }
4462         var pieceArea = new java.awt.geom.Area(ShapeTools.getShape(piece.getPoints(), true, null));
4463         if (furnitureGroupsArea == null) {
4464           furnitureGroupsArea = groupArea;
4465           furnitureInGroupsArea = pieceArea;
4466         } else {
4467           if (lastGroup !== homePieceOfFurniture) {
4468             furnitureGroupsArea.add(groupArea);
4469           }
4470           furnitureInGroupsArea.add(pieceArea);
4471         }
4472         lastGroup = homePieceOfFurniture;
4473       }
4474       paintedFurniture.push(piece);
4475     }
4476   }
4477   if (furnitureGroupsArea != null) {
4478     furnitureGroupsArea.subtract(furnitureInGroupsArea);
4479     var oldComposite = this.setTransparency(g2D, 0.6);
4480     g2D.setPaint(selectionOutlinePaint);
4481     g2D.fill(furnitureGroupsArea);
4482     g2D.setAlpha(oldComposite);
4483   }
4484   
4485   paintedFurniture.forEach(function(piece) {
4486       var points = piece.getPoints();
4487       var pieceShape = ShapeTools.getShape(points, true, null);
4488       
4489       g2D.setPaint(selectionOutlinePaint);
4490       g2D.setStroke(selectionOutlineStroke);
4491       g2D.draw(pieceShape);
4492       
4493       g2D.setPaint(foregroundColor);
4494       g2D.setStroke(pieceBorderStroke);
4495       g2D.draw(pieceShape);
4496       
4497       g2D.setStroke(pieceFrontBorderStroke);
4498       g2D.draw(new java.awt.geom.Line2D.Float(points[2][0], points[2][1], points[3][0], points[3][1]));
4499       
4500       if (items.length === 1 && indicatorPaint != null) {
4501         plan.paintPieceOfFurnitureIndicators(g2D, piece, indicatorPaint, planScale);
4502       }
4503     });
4504 }
4505 
4506 /**
4507  * Returns <code>piece</code> if it belongs to home furniture or the group to which <code>piece</code> belongs.
4508  * @param {HomePieceOfFurniture} piece
4509  * @param {HomePieceOfFurniture[]} homeFurniture
4510  * @return {HomePieceOfFurniture}
4511  * @private
4512  */
4513 PlanComponent.prototype.getPieceOfFurnitureInHomeFurniture = function(piece, homeFurniture) {
4514   if (!(homeFurniture.indexOf((piece)) >= 0)) {
4515     for (var i = 0; i < homeFurniture.length; i++) {
4516       var homePiece = homeFurniture[i];
4517       if ((homePiece instanceof HomeFurnitureGroup) 
4518           && ((homePiece).getAllFurniture().indexOf((piece)) >= 0)) {
4519         return homePiece;
4520       }
4521     }
4522   }
4523   return piece;
4524 }
4525 
4526 /**
4527  * Paints <code>icon</code> with <code>g2D</code>.
4528  * @param {Graphics2D} g2D
4529  * @param {HomePieceOfFurniture} piece
4530  * @param {PlanComponent.PieceOfFurnitureTopViewIcon} icon
4531  * @param {Object} pieceShape2D
4532  * @param {number} planScale
4533  * @param {string} backgroundColor
4534  * @private
4535  */
4536 PlanComponent.prototype.paintPieceOfFurnitureIcon = function(g2D, piece, icon, pieceShape2D, planScale, backgroundColor) {
4537   var plan = this;
4538   if (icon == null) {
4539     if (this.furnitureIconsCache == null) {
4540       this.furnitureIconsCache = {};
4541     }
4542     var image = this.furnitureIconsCache[piece.icon.getURL()];
4543     if (image == null) {
4544       image = TextureManager.getInstance().getWaitImage();
4545       TextureManager.getInstance().loadTexture(piece.icon, {
4546           textureUpdated: function(texture) {
4547             plan.furnitureIconsCache[piece.icon.getURL()] = texture;
4548             plan.repaint();
4549           },
4550           textureError: function() {
4551             plan.furnitureIconsCache[piece.icon.getURL()] = TextureManager.getInstance().getErrorImage();
4552             plan.repaint();
4553           }
4554         });
4555     }
4556     icon = new PlanComponent.PieceOfFurnitureTopViewIcon(image);
4557   }
4558   
4559   // Fill piece area
4560   g2D.setPaint(backgroundColor);
4561   g2D.fill(pieceShape2D);
4562   var previousClip = g2D.getClip();
4563   // Clip icon drawing into piece shape
4564   g2D.clip(pieceShape2D);
4565   var previousTransform = g2D.getTransform();
4566   // Translate to piece center
4567   var bounds = pieceShape2D.getBounds2D();
4568   g2D.translate(bounds.getCenterX(), bounds.getCenterY());
4569   var pieceDepth = piece.getDepthInPlan();
4570   if (piece instanceof HomeDoorOrWindow) {
4571     pieceDepth *= piece.getWallThickness();
4572   }
4573   // Scale icon to fit in its area
4574   var minDimension = Math.min(piece.getWidthInPlan(), pieceDepth);
4575   var iconScale = Math.min(1 / planScale, minDimension / icon.getIconHeight());
4576   // If piece model is mirrored, inverse x scale
4577   if (piece.isModelMirrored()) {
4578     g2D.scale(-iconScale, iconScale);
4579   } else {
4580     g2D.scale(iconScale, iconScale);
4581   }
4582   // Paint piece icon
4583   icon.paintIcon(g2D, -icon.getIconWidth() / 2 | 0, -icon.getIconHeight() / 2 | 0);
4584   // Revert g2D transformation to previous value
4585   g2D.setTransform(previousTransform);
4586   g2D.setClip(previousClip);
4587 }
4588 
4589 /**
4590  * Paints <code>piece</code> top icon with <code>g2D</code>.
4591  * @param {Graphics2D} g2D
4592  * @param {HomePieceOfFurniture} piece
4593  * @param {Object} pieceShape2D
4594  * @param {java.awt.BasicStroke} pieceBorderStroke
4595  * @param {number} planScale
4596  * @param {string} backgroundColor
4597  * @param {string} foregroundColor
4598  * @param {PlanComponent.PaintMode} paintMode
4599  * @private
4600  */
4601 PlanComponent.prototype.paintPieceOfFurnitureTop = function(g2D, piece, pieceShape2D, pieceBorderStroke, planScale, backgroundColor, foregroundColor, paintMode) {
4602   if (this.furnitureTopViewIconKeys == null) {
4603     this.furnitureTopViewIconKeys = {};
4604     this.furnitureTopViewIconsCache = {};
4605   }
4606   var topViewIconKey = CoreTools.getFromMap(this.furnitureTopViewIconKeys, piece);
4607   var icon;
4608   if (topViewIconKey == null) {
4609     topViewIconKey = new PlanComponent.HomePieceOfFurnitureTopViewIconKey(piece.clone());
4610     icon = CoreTools.getFromMap(this.furnitureTopViewIconsCache, topViewIconKey);
4611     if (icon == null 
4612         || icon.isWaitIcon() 
4613            && paintMode !== PlanComponent.PaintMode.PAINT) {
4614       var waitingComponent = paintMode === PlanComponent.PaintMode.PAINT ? this : null;
4615       if (piece.getPlanIcon() != null) {
4616         icon = new PlanComponent.PieceOfFurniturePlanIcon(piece, waitingComponent);
4617       } else {
4618         icon = new PlanComponent.PieceOfFurnitureModelIcon(piece, this.object3dFactory, waitingComponent, this.preferences.getFurnitureModelIconSize());
4619       }
4620       CoreTools.putToMap(this.furnitureTopViewIconsCache, topViewIconKey, icon);
4621     } else {
4622       for (var i = 0; i < this.furnitureTopViewIconsCache.entries.length; i++) { // Parse keySet
4623         var key = this.furnitureTopViewIconsCache.entries[i].key;
4624         if (key.equals(topViewIconKey)) {
4625           topViewIconKey = key;
4626           break;
4627         }
4628       }
4629     }
4630     CoreTools.putToMap(this.furnitureTopViewIconKeys, piece, topViewIconKey);
4631   } else {
4632     icon = CoreTools.getFromMap(this.furnitureTopViewIconsCache, topViewIconKey);
4633   }
4634   if (icon.isWaitIcon() || icon.isErrorIcon()) {
4635     this.paintPieceOfFurnitureIcon(g2D, piece, icon, pieceShape2D, planScale, backgroundColor);
4636     g2D.setPaint(foregroundColor);
4637     g2D.setStroke(pieceBorderStroke);
4638     g2D.draw(pieceShape2D);
4639   } else {
4640     var previousTransform = g2D.getTransform();
4641     var bounds = pieceShape2D.getBounds2D();
4642     g2D.translate(bounds.getCenterX(), bounds.getCenterY());
4643     g2D.rotate(piece.getAngle());
4644     var pieceDepth = piece.getDepthInPlan();
4645     if (piece.isModelMirrored()
4646         && piece.getRoll() == 0) {
4647       g2D.scale(-piece.getWidthInPlan() / icon.getIconWidth(), pieceDepth / icon.getIconHeight());
4648     } else {
4649       g2D.scale(piece.getWidthInPlan() / icon.getIconWidth(), pieceDepth / icon.getIconHeight());
4650     }
4651     icon.paintIcon(g2D, (-icon.getIconWidth() / 2 | 0), (-icon.getIconHeight() / 2 | 0));
4652     g2D.setTransform(previousTransform);
4653   }
4654 }
4655 
4656 /**
4657  * Paints rotation, elevation, height and resize indicators on <code>piece</code>.
4658  * @param {Graphics2D} g2D
4659  * @param {HomePieceOfFurniture} piece
4660  * @param {string|CanvasPattern} indicatorPaint
4661  * @param {number} planScale
4662  * @private
4663  */
4664 PlanComponent.prototype.paintPieceOfFurnitureIndicators = function(g2D, piece, indicatorPaint, planScale) {
4665   if (this.resizeIndicatorVisible) {
4666     g2D.setPaint(indicatorPaint);
4667     g2D.setStroke(PlanComponent.INDICATOR_STROKE);
4668     
4669     var previousTransform = g2D.getTransform();
4670     var piecePoints = piece.getPoints();
4671     var scaleInverse = 1 / planScale;
4672     var pieceAngle = piece.getAngle();
4673     var rotationIndicator = this.getIndicator(piece, PlanComponent.IndicatorType.ROTATE);
4674     if (rotationIndicator != null) {
4675       g2D.translate(piecePoints[0][0], piecePoints[0][1]);
4676       g2D.scale(scaleInverse, scaleInverse);
4677       g2D.rotate(pieceAngle);
4678       g2D.draw(rotationIndicator);
4679       g2D.setTransform(previousTransform);
4680     }
4681     
4682     var elevationIndicator = this.getIndicator(piece, PlanComponent.IndicatorType.ELEVATE);
4683     if (elevationIndicator != null) {
4684       g2D.translate(piecePoints[1][0], piecePoints[1][1]);
4685       g2D.scale(scaleInverse, scaleInverse);
4686       g2D.rotate(pieceAngle);
4687       g2D.draw(PlanComponent.ELEVATION_POINT_INDICATOR);
4688       g2D.translate(6.5, -6.5);
4689       g2D.rotate(-pieceAngle);
4690       g2D.draw(elevationIndicator);
4691       g2D.setTransform(previousTransform);
4692     }
4693     
4694     g2D.translate(piecePoints[3][0], piecePoints[3][1]);
4695     g2D.scale(scaleInverse, scaleInverse);
4696     g2D.rotate(pieceAngle);
4697     if (piece.getPitch() !== 0 && this.isFurnitureSizeInPlanSupported()) {
4698       var pitchIndicator = this.getIndicator(piece, PlanComponent.IndicatorType.ROTATE_PITCH);
4699       if (pitchIndicator != null) {
4700         g2D.draw(pitchIndicator);
4701       }
4702     } else if (piece.getRoll() !== 0 && this.isFurnitureSizeInPlanSupported()) {
4703       var rollIndicator = this.getIndicator(piece, PlanComponent.IndicatorType.ROTATE_ROLL);
4704       if (rollIndicator != null) {
4705         g2D.draw(rollIndicator);
4706       }
4707     } else if (piece instanceof HomeLight) {
4708       var powerIndicator = this.getIndicator(piece, PlanComponent.IndicatorType.CHANGE_POWER);
4709       if (powerIndicator != null) {
4710         g2D.draw(PlanComponent.LIGHT_POWER_POINT_INDICATOR);
4711         g2D.translate(-7.5, 7.5);
4712         g2D.rotate(-pieceAngle);
4713         g2D.draw(powerIndicator);
4714       }
4715     } else if (piece.isResizable() && !piece.isHorizontallyRotated()) {
4716       var heightIndicator = this.getIndicator(piece, PlanComponent.IndicatorType.RESIZE_HEIGHT);
4717       if (heightIndicator != null) {
4718         g2D.draw(PlanComponent.HEIGHT_POINT_INDICATOR);
4719         g2D.translate(-7.5, 7.5);
4720         g2D.rotate(-pieceAngle);
4721         g2D.draw(heightIndicator);
4722       }
4723     }
4724     g2D.setTransform(previousTransform);
4725     if (piece.isResizable()) {
4726       var resizeIndicator = this.getIndicator(piece, PlanComponent.IndicatorType.RESIZE);
4727       if (resizeIndicator != null) {
4728         g2D.translate(piecePoints[2][0], piecePoints[2][1]);
4729         g2D.scale(scaleInverse, scaleInverse);
4730         g2D.rotate(pieceAngle);
4731         g2D.draw(resizeIndicator);
4732         g2D.setTransform(previousTransform);
4733       }
4734     }
4735     
4736     if (piece.isNameVisible() 
4737         && piece.getName().trim().length > 0) {
4738       var xName = piece.getX() + piece.getNameXOffset();
4739       var yName = piece.getY() + piece.getNameYOffset();
4740       this.paintTextIndicators(g2D, piece, this.getLineCount(piece.getName()), piece.getNameStyle(), xName, yName, piece.getNameAngle(), indicatorPaint, planScale);
4741     }
4742   }
4743 }
4744 
4745 /**
4746  * Paints polylines.
4747  * @param {Graphics2D} g2D
4748  * @param {Polyline[]} polylines
4749  * @param {Object[]} selectedItems
4750  * @param {Level} level
4751  * @param {string|CanvasPattern} selectionOutlinePaint
4752  * @param {string|CanvasPattern} indicatorPaint
4753  * @param {number} planScale
4754  * @param {string} foregroundColor
4755  * @param {PlanComponent.PaintMode} paintMode
4756  * @private
4757  */
4758 PlanComponent.prototype.paintPolylines = function(g2D, polylines, selectedItems, level, selectionOutlinePaint, indicatorPaint, planScale, foregroundColor, paintMode) {
4759   for (var i = 0; i < polylines.length; i++) {
4760     var polyline = polylines[i];
4761     if (this.isViewableAtLevel(polyline, level)) {
4762       var selected = (selectedItems.indexOf((polyline)) >= 0);
4763       if (paintMode !== PlanComponent.PaintMode.CLIPBOARD || selected) {
4764         g2D.setPaint(ColorTools.integerToHexadecimalString(polyline.getColor()));
4765         var thickness = polyline.getThickness();
4766         g2D.setStroke(ShapeTools.getStroke(thickness, polyline.getCapStyle(), polyline.getJoinStyle(), 
4767             polyline.getDashStyle() !== Polyline.DashStyle.SOLID ? polyline.getDashPattern() : null, // null renders better closed shapes with a solid style 
4768             polyline.getDashOffset()));
4769         var polylineShape = ShapeTools.getPolylineShape(polyline.getPoints(), 
4770             polyline.getJoinStyle() === Polyline.JoinStyle.CURVED, polyline.isClosedPath());
4771         g2D.draw(polylineShape);
4772         
4773         var firstPoint = null;
4774         var secondPoint = null;
4775         var beforeLastPoint = null;
4776         var lastPoint = null;
4777         for (var it = polylineShape.getPathIterator(null, 0.5); !it.isDone(); it.next()) {
4778           var pathPoint = [0, 0];
4779           if (it.currentSegment(pathPoint) !== java.awt.geom.PathIterator.SEG_CLOSE) {
4780             if (firstPoint == null) {
4781               firstPoint = pathPoint;
4782             } else if (secondPoint == null) {
4783               secondPoint = pathPoint;
4784             }
4785             beforeLastPoint = lastPoint;
4786             lastPoint = pathPoint;
4787           }
4788         }
4789         var angleAtStart = Math.atan2(firstPoint[1] - secondPoint[1], 
4790             firstPoint[0] - secondPoint[0]);
4791         var angleAtEnd = Math.atan2(lastPoint[1] - beforeLastPoint[1], 
4792             lastPoint[0] - beforeLastPoint[0]);
4793         var arrowDelta = polyline.getCapStyle() !== Polyline.CapStyle.BUTT 
4794             ? thickness / 2 
4795             : 0;
4796         this.paintArrow(g2D, firstPoint, angleAtStart, polyline.getStartArrowStyle(), thickness, arrowDelta);
4797         this.paintArrow(g2D, lastPoint, angleAtEnd, polyline.getEndArrowStyle(), thickness, arrowDelta);
4798         
4799         if (selected && paintMode === PlanComponent.PaintMode.PAINT) {
4800           g2D.setPaint(selectionOutlinePaint);
4801           g2D.setStroke(ShapeTools.getStroke(thickness + 4 / planScale, 
4802               polyline.getCapStyle(), polyline.getJoinStyle(), null));
4803           g2D.draw(polylineShape);
4804           
4805           if (selectedItems.length === 1 
4806               && indicatorPaint != null) {
4807             var selectedPolyline = selectedItems[0];
4808             if (this.isViewableAtLevel(polyline, level)) {
4809               g2D.setPaint(indicatorPaint);
4810               this.paintPointsResizeIndicators(g2D, selectedPolyline, indicatorPaint, planScale, 
4811                   selectedPolyline.isClosedPath(), angleAtStart, angleAtEnd, false);
4812             }
4813           }
4814         }
4815       }
4816     }
4817   }
4818 }
4819 
4820 /**
4821  * Paints polyline arrow at the given point and orientation.
4822  * @param {Graphics2D} g2D
4823  * @param {Array} point
4824  * @param {number} angle
4825  * @param {Polyline.ArrowStyle} arrowStyle
4826  * @param {number} thickness
4827  * @param {number} arrowDelta
4828  * @private
4829  */
4830 PlanComponent.prototype.paintArrow = function(g2D, point, angle, arrowStyle, thickness, arrowDelta) {
4831   if (arrowStyle != null 
4832       && arrowStyle !== Polyline.ArrowStyle.NONE) {
4833     var oldTransform = g2D.getTransform();
4834     g2D.translate(point[0], point[1]);
4835     g2D.rotate(angle);
4836     g2D.translate(arrowDelta, 0);
4837     var scale = Math.pow(thickness, 0.66) * 2;
4838     g2D.scale(scale, scale);
4839     switch ((arrowStyle)) {
4840       case Polyline.ArrowStyle.DISC:
4841         g2D.fill(new java.awt.geom.Ellipse2D.Float(-3.5, -2, 4, 4));
4842         break;
4843       case Polyline.ArrowStyle.OPEN:
4844         g2D.scale(0.9, 0.9);
4845         g2D.setStroke(new java.awt.BasicStroke((thickness / scale / 0.9), java.awt.BasicStroke.CAP_BUTT, java.awt.BasicStroke.JOIN_MITER));
4846         g2D.draw(PlanComponent.ARROW);
4847         break;
4848       case Polyline.ArrowStyle.DELTA:
4849         g2D.translate(1.65, 0);
4850         g2D.fill(PlanComponent.ARROW);
4851         break;
4852       default:
4853         break;
4854     }
4855     g2D.setTransform(oldTransform);
4856   }
4857 }
4858 
4859 /**
4860  * Paints dimension lines.
4861  * @param {Graphics2D} g2D
4862  * @param {DimensionLine[]} dimensionLines
4863  * @param {Object[]} selectedItems
4864  * @param {Level} level
4865  * @param {string|CanvasPattern} selectionOutlinePaint
4866  * @param {java.awt.BasicStroke} selectionOutlineStroke
4867  * @param {string|CanvasPattern} indicatorPaint
4868  * @param {java.awt.BasicStroke} extensionLineStroke
4869  * @param {number} planScale
4870  * @param {string} backgroundColor
4871  * @param {string} foregroundColor
4872  * @param {PlanComponent.PaintMode} paintMode
4873  * @param {boolean} feedback
4874  * @private
4875  */
4876 PlanComponent.prototype.paintDimensionLines = function(g2D, dimensionLines, selectedItems, level, selectionOutlinePaint, selectionOutlineStroke, indicatorPaint, extensionLineStroke, planScale, backgroundColor, foregroundColor, paintMode, feedback) {
4877   var plan = this;
4878   if (paintMode === PlanComponent.PaintMode.CLIPBOARD) {
4879     dimensionLines = Home.getDimensionLinesSubList(selectedItems);
4880   }
4881   var markEndWidth = PlanComponent.DIMENSION_LINE_MARK_END.getBounds2D().getWidth();
4882   var selectedDimensionLineWithIndicators = selectedItems.length == 1
4883          && selectedItems[0] instanceof DimensionLine
4884          && paintMode === PlanComponent.PaintMode.PAINT
4885          && indicatorPaint != null
4886       ? selectedItems[0]
4887       : null;
4888 
4889   var previousFont = g2D.getFont();
4890   for (var i = 0; i < dimensionLines.length; i++) {
4891     var dimensionLine = dimensionLines[i];
4892     if (plan.isViewableAtLevel(dimensionLine, level)) {
4893       var dimensionLineColor = dimensionLine.getColor();
4894       var markEndScale = dimensionLine.getEndMarkSize() / markEndWidth;
4895       var dimensionLineStroke = new java.awt.BasicStroke(plan.getStrokeWidth(DimensionLine, paintMode) / markEndScale / planScale);
4896       g2D.setPaint(dimensionLineColor != null ? ColorTools.integerToHexadecimalString(dimensionLineColor) : foregroundColor);
4897       var previousTransform = g2D.getTransform();
4898       var elevationDimensionLine = dimensionLine.isElevationDimensionLine();
4899       var angle = elevationDimensionLine
4900           ? (dimensionLine.getPitch() + 2 * Math.PI) % (2 * Math.PI)
4901           : Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(), dimensionLine.getXEnd() - dimensionLine.getXStart());
4902       var dimensionLineOffset = dimensionLine.getOffset();
4903       var dimensionLineLength = dimensionLine.getLength();
4904       g2D.translate(dimensionLine.getXStart(), dimensionLine.getYStart());
4905       g2D.rotate(angle);
4906       g2D.translate(0, dimensionLineOffset);
4907         
4908       var horizontalDimensionLine = dimensionLine.getElevationStart() == dimensionLine.getElevationEnd();
4909       if (paintMode === PlanComponent.PaintMode.PAINT 
4910           && plan.selectedItemsOutlinePainted 
4911           && (selectedItems.indexOf((dimensionLine)) >= 0)) {
4912         g2D.setPaint(selectionOutlinePaint);
4913         g2D.setStroke(selectionOutlineStroke);
4914         if (horizontalDimensionLine) {
4915           g2D.draw(new java.awt.geom.Line2D.Float(0, 0, dimensionLineLength, 0));
4916           g2D.scale(markEndScale, markEndScale);
4917           g2D.draw(PlanComponent.DIMENSION_LINE_MARK_END);
4918           g2D.translate(dimensionLineLength / markEndScale, 0);
4919           g2D.draw(PlanComponent.DIMENSION_LINE_MARK_END);
4920           g2D.scale(1 / markEndScale, 1 / markEndScale);
4921           g2D.translate(-dimensionLineLength, 0);
4922           g2D.draw(new java.awt.geom.Line2D.Float(0, -dimensionLineOffset, 0, 0));
4923           g2D.draw(new java.awt.geom.Line2D.Float(dimensionLineLength, -dimensionLineOffset, dimensionLineLength, 0));
4924         } else {
4925           g2D.scale(markEndScale, markEndScale);
4926           g2D.draw(PlanComponent.VERTICAL_DIMENSION_LINE);
4927           g2D.scale(1 / markEndScale, 1 / markEndScale);
4928           if (Math.abs(dimensionLineOffset) > dimensionLine.getEndMarkSize() / 2) {
4929             g2D.draw(new java.awt.geom.Line2D.Float(0, -dimensionLineOffset,
4930                 0, -dimensionLine.getEndMarkSize() / 2 * (dimensionLineOffset >= 0 ? (dimensionLineOffset == 0 ? 0 : 1) : -1)));
4931           }
4932         }
4933         g2D.setPaint(dimensionLineColor != null ? ColorTools.integerToHexadecimalString(dimensionLineColor) : foregroundColor);
4934       }
4935         
4936       g2D.setStroke(dimensionLineStroke);
4937       if (horizontalDimensionLine) {
4938         g2D.draw(new java.awt.geom.Line2D.Float(0, 0, dimensionLineLength, 0));
4939         g2D.scale(markEndScale, markEndScale);
4940         g2D.draw(PlanComponent.DIMENSION_LINE_MARK_END);
4941         g2D.translate(dimensionLineLength / markEndScale, 0);
4942         g2D.draw(PlanComponent.DIMENSION_LINE_MARK_END);
4943         g2D.scale(1 / markEndScale, 1 / markEndScale);
4944         g2D.translate(-dimensionLineLength, 0);
4945         g2D.setStroke(extensionLineStroke);
4946         g2D.draw(new java.awt.geom.Line2D.Float(0, -dimensionLineOffset, 0, 0));
4947         g2D.draw(new java.awt.geom.Line2D.Float(dimensionLineLength, -dimensionLineOffset, dimensionLineLength, 0));
4948       } else {
4949         g2D.scale(markEndScale, markEndScale);
4950         g2D.fill(PlanComponent.VERTICAL_DIMENSION_LINE_DISC);
4951         g2D.draw(PlanComponent.VERTICAL_DIMENSION_LINE);
4952         g2D.scale(1 / markEndScale, 1 / markEndScale);
4953         g2D.setStroke(extensionLineStroke);
4954         if (Math.abs(dimensionLineOffset) > dimensionLine.getEndMarkSize() / 2) {
4955           g2D.draw(new java.awt.geom.Line2D.Float(0, -dimensionLineOffset,
4956               0, -dimensionLine.getEndMarkSize() / 2 * (dimensionLineOffset >= 0 ? (dimensionLineOffset == 0 ? 0 : 1) : -1)));
4957         }
4958       }
4959         
4960       if (horizontalDimensionLine
4961           || dimensionLine === selectedDimensionLineWithIndicators) {
4962         var lengthText = plan.preferences.getLengthUnit().getFormat().format(dimensionLineLength);
4963         var lengthStyle = dimensionLine.getLengthStyle();
4964         if (lengthStyle == null) {
4965           lengthStyle = plan.preferences.getDefaultTextStyle(dimensionLine.constructor);
4966         }
4967         if (feedback && plan.getFont() != null
4968             || !horizontalDimensionLine
4969                 && dimensionLine == selectedDimensionLineWithIndicators) {
4970           // Call directly the overloaded deriveStyle method that takes a float parameter 
4971           // to avoid confusion with the one that takes a TextStyle.Alignment parameter
4972           lengthStyle = lengthStyle.deriveStyle$float(parseInt(new Font(plan.getFont()).size) / planScale);
4973         }
4974         var font = plan.getFont(previousFont, lengthStyle);
4975         var lengthFontMetrics = plan.getFontMetrics(font, lengthStyle);
4976         var lengthTextBounds = lengthFontMetrics.getStringBounds(lengthText, g2D);
4977         g2D.setFont(font);
4978         if (!horizontalDimensionLine
4979             && dimensionLine === selectedDimensionLineWithIndicators) {
4980           g2D.rotate(angle > Math.PI ? Math.PI / 2 : -Math.PI / 2);
4981           g2D.translate(dimensionLineOffset <= 0 ^ angle <= Math.PI
4982                 ? -lengthTextBounds.getWidth() - markEndWidth / 2 - 5 / planScale
4983                 : markEndWidth / 2 + 5 / planScale,
4984               lengthFontMetrics.getAscent() / 2);
4985           if (elevationDimensionLine
4986               && this.resizeIndicatorVisible) {
4987             // Add room for pitch rotation indicator
4988             g2D.translate((dimensionLineOffset <= 0 ^ angle <= Math.PI ? -1 : 1) * 10 / planScale, 0);
4989           }
4990         } else {
4991           g2D.translate((dimensionLineLength - lengthTextBounds.getWidth()) / 2,
4992               dimensionLineOffset <= 0
4993                   ? -lengthFontMetrics.getDescent() - 1
4994                   : lengthFontMetrics.getAscent() + 1);
4995         }
4996         if (feedback
4997             || !horizontalDimensionLine
4998                 && dimensionLine === selectedDimensionLineWithIndicators) {
4999           g2D.setColor(backgroundColor);
5000           var oldComposite = plan.setTransparency(g2D, 0.7);
5001           g2D.setStroke(new java.awt.BasicStroke(4 / planScale, java.awt.BasicStroke.CAP_SQUARE, java.awt.BasicStroke.CAP_ROUND));
5002           g2D.drawStringOutline(lengthText, 0, 0);
5003           g2D.setAlpha(oldComposite);
5004           g2D.setColor(foregroundColor);
5005           if (!feedback) {
5006             g2D.setPaint(indicatorPaint);
5007           }
5008         }
5009         g2D.setFont(font);
5010         g2D.drawString(lengthText, 0, 0);
5011       }
5012       g2D.setTransform(previousTransform);
5013     }
5014   }
5015   g2D.setFont(previousFont);
5016   if (selectedDimensionLineWithIndicators != null) {
5017     this.paintDimensionLineResizeIndicators(g2D, selectedDimensionLineWithIndicators, indicatorPaint, planScale);
5018   }
5019 }
5020 
5021 /**
5022  * Paints resize indicators on a given dimension line.
5023  * @param {Graphics2D} g2D
5024  * @param {DimensionLine} dimensionLine
5025  * @param {string|CanvasPattern} indicatorPaint
5026  * @param {number} planScale
5027  * @private
5028  */
5029 PlanComponent.prototype.paintDimensionLineResizeIndicators = function(g2D, dimensionLine, indicatorPaint, planScale) {
5030   if (this.resizeIndicatorVisible) {
5031     g2D.setPaint(indicatorPaint);
5032     g2D.setStroke(PlanComponent.INDICATOR_STROKE);
5033     
5034     var dimensionLineAngle = dimensionLine.isElevationDimensionLine()
5035         ? dimensionLine.getPitch()
5036         : Math.atan2(dimensionLine.getYEnd() - dimensionLine.getYStart(), dimensionLine.getXEnd() - dimensionLine.getXStart());
5037     var horizontalDimensionLine = dimensionLine.getElevationStart() === dimensionLine.getElevationEnd();
5038     
5039     var previousTransform = g2D.getTransform();
5040     var scaleInverse = 1 / planScale;
5041     var resizeIndicator = this.getIndicator(dimensionLine, PlanComponent.IndicatorType.RESIZE);
5042     if (horizontalDimensionLine) {
5043       g2D.translate(dimensionLine.getXStart(), dimensionLine.getYStart());
5044       g2D.rotate(dimensionLineAngle);
5045       g2D.translate(0, dimensionLine.getOffset());
5046       g2D.rotate(Math.PI);
5047       g2D.scale(scaleInverse, scaleInverse);
5048       g2D.draw(resizeIndicator);
5049       g2D.setTransform(previousTransform);
5050     
5051       g2D.translate(dimensionLine.getXEnd(), dimensionLine.getYEnd());
5052       g2D.rotate(dimensionLineAngle);
5053       g2D.translate(0, dimensionLine.getOffset());
5054       g2D.scale(scaleInverse, scaleInverse);
5055       g2D.draw(resizeIndicator);
5056       g2D.setTransform(previousTransform);
5057     
5058       g2D.translate((dimensionLine.getXStart() + dimensionLine.getXEnd()) / 2, 
5059           (dimensionLine.getYStart() + dimensionLine.getYEnd()) / 2);
5060     } else {
5061       g2D.translate(dimensionLine.getXStart(), dimensionLine.getYStart());
5062     }
5063     
5064     g2D.rotate(dimensionLineAngle);
5065     var middlePointTransform = g2D.getTransform();
5066     g2D.translate(0, dimensionLine.getOffset()
5067         - (horizontalDimensionLine ? 0 : dimensionLine.getEndMarkSize() / 2 * (dimensionLine.getOffset() > 0 ? 1 : -1)));
5068     g2D.rotate(dimensionLine.getOffset() <= 0 
5069         ? Math.PI / 2 
5070         : -Math.PI / 2);
5071     g2D.scale(scaleInverse, scaleInverse);
5072     g2D.draw(resizeIndicator);
5073 
5074     if (!horizontalDimensionLine) {
5075       if (dimensionLine.isElevationDimensionLine()) {
5076         g2D.setTransform(middlePointTransform);        
5077         g2D.translate(0, dimensionLine.getOffset() + dimensionLine.getEndMarkSize() / 2 * (dimensionLine.getOffset() > 0 ? 1 : -1));
5078         g2D.rotate(dimensionLine.getOffset() <= 0
5079             ? Math.PI / 2
5080             : -Math.PI / 2);
5081         g2D.scale(scaleInverse, scaleInverse);
5082         g2D.draw(this.getIndicator(dimensionLine, PlanComponent.IndicatorType.ROTATE));
5083       }
5084 
5085       g2D.setTransform(middlePointTransform);      
5086       g2D.translate(-dimensionLine.getEndMarkSize() / 2, dimensionLine.getOffset());
5087       g2D.scale(scaleInverse, scaleInverse);
5088       g2D.draw(PlanComponent.ELEVATION_POINT_INDICATOR);
5089       g2D.translate(-9, 0);
5090       g2D.rotate(-dimensionLineAngle);
5091       g2D.draw(this.getIndicator(dimensionLine, PlanComponent.IndicatorType.ELEVATE));
5092 
5093       g2D.setTransform(middlePointTransform);
5094       g2D.translate(5, dimensionLine.getOffset());
5095       g2D.scale(scaleInverse, scaleInverse);
5096       g2D.draw(PlanComponent.HEIGHT_POINT_INDICATOR);
5097       g2D.translate(10, 0);
5098       g2D.rotate(-dimensionLineAngle);
5099       g2D.draw(this.getIndicator(dimensionLine, PlanComponent.IndicatorType.RESIZE_HEIGHT));
5100     }
5101     
5102     g2D.setTransform(previousTransform);
5103   }
5104 }
5105 
5106 /**
5107  * Paints home labels.
5108  * @param {Graphics2D} g2D
5109  * @param {Label[]} labels
5110  * @param {Object[]} selectedItems
5111  * @param {Level} level
5112  * @param {string|CanvasPattern} selectionOutlinePaint
5113  * @param {java.awt.BasicStroke} selectionOutlineStroke
5114  * @param {string|CanvasPattern} indicatorPaint
5115  * @param {number} planScale
5116  * @param {string} foregroundColor
5117  * @param {PlanComponent.PaintMode} paintMode
5118  * @private
5119  */
5120 PlanComponent.prototype.paintLabels = function(g2D, labels, selectedItems, level, selectionOutlinePaint, selectionOutlineStroke, indicatorPaint, planScale, foregroundColor, paintMode) {
5121   var previousFont = g2D.getFont();
5122   for (var i = 0; i < labels.length; i++) {
5123     var label = labels[i];
5124     if (this.isViewableAtLevel(label, level)) {
5125       var selectedLabel = (selectedItems.indexOf((label)) >= 0);
5126       if (paintMode !== PlanComponent.PaintMode.CLIPBOARD || selectedLabel) {
5127         var labelText = label.getText();
5128         var xLabel = label.getX();
5129         var yLabel = label.getY();
5130         var labelAngle = label.getAngle();
5131         var labelStyle = label.getStyle();
5132         if (labelStyle == null) {
5133           labelStyle = this.preferences.getDefaultTextStyle(label.constructor);
5134         }
5135         if (labelStyle.getFontName() == null && this.getFont() != null) {
5136           labelStyle = labelStyle.deriveStyle(new Font(this.getFont()).family);
5137         }
5138         var color = label.getColor();
5139         g2D.setPaint(color != null ? ColorTools.integerToHexadecimalString(color) : foregroundColor);
5140         this.paintText(g2D, label.constructor, labelText, labelStyle, label.getOutlineColor(), 
5141             xLabel, yLabel, labelAngle, previousFont);
5142         
5143         if (paintMode === PlanComponent.PaintMode.PAINT && this.selectedItemsOutlinePainted && selectedLabel) {
5144           g2D.setPaint(selectionOutlinePaint);
5145           g2D.setStroke(selectionOutlineStroke);
5146           var textBounds = this.getTextBounds(labelText, labelStyle, xLabel, yLabel, labelAngle);
5147           g2D.draw(ShapeTools.getShape(textBounds, true, null));
5148           g2D.setPaint(foregroundColor);
5149           if (indicatorPaint != null && selectedItems.length === 1 && selectedItems[0] === label) {
5150             this.paintTextIndicators(g2D, label, this.getLineCount(labelText), 
5151                 labelStyle, xLabel, yLabel, labelAngle, indicatorPaint, planScale);
5152             
5153             if (this.resizeIndicatorVisible 
5154                 && label.getPitch() != null) {
5155               var elevationIndicator = this.getIndicator(label, PlanComponent.IndicatorType.ELEVATE);
5156               if (elevationIndicator != null) {
5157                 var previousTransform = g2D.getTransform();
5158                 if (labelStyle.getAlignment() === TextStyle.Alignment.LEFT) {
5159                   g2D.translate(textBounds[3][0], textBounds[3][1]);
5160                 } else if (labelStyle.getAlignment() === TextStyle.Alignment.RIGHT) {
5161                   g2D.translate(textBounds[2][0], textBounds[2][1]);
5162                 } else {
5163                   g2D.translate((textBounds[2][0] + textBounds[3][0]) / 2, (textBounds[2][1] + textBounds[3][1]) / 2);
5164                 }
5165                 var scaleInverse = 1 / planScale;
5166                 g2D.scale(scaleInverse, scaleInverse);
5167                 g2D.rotate(label.getAngle());
5168                 g2D.draw(PlanComponent.ELEVATION_POINT_INDICATOR);
5169                 g2D.translate(0, 10.0);
5170                 g2D.rotate(-label.getAngle());
5171                 g2D.draw(elevationIndicator);
5172                 g2D.setTransform(previousTransform);
5173               }
5174             }
5175           }
5176         }
5177       }
5178     }
5179   }
5180   g2D.setFont(previousFont);
5181 }
5182 
5183 /**
5184  * Paints the compass.
5185  * @param {Graphics2D} g2D
5186  * @param {Object[]} selectedItems
5187  * @param {number} planScale
5188  * @param {string} foregroundColor
5189  * @param {PlanComponent.PaintMode} paintMode
5190  * @private
5191  */
5192 PlanComponent.prototype.paintCompass = function(g2D, selectedItems, planScale, foregroundColor, paintMode) {
5193   var compass = this.home.getCompass();
5194   if (compass.isVisible() 
5195       && (paintMode !== PlanComponent.PaintMode.CLIPBOARD 
5196           || selectedItems.indexOf(compass) >= 0)) {
5197     var previousTransform = g2D.getTransform();
5198     g2D.translate(compass.getX(), compass.getY());
5199     g2D.rotate(compass.getNorthDirection());
5200     var diameter = compass.getDiameter();
5201     g2D.scale(diameter, diameter);
5202     g2D.setColor(foregroundColor);
5203     g2D.fill(PlanComponent.COMPASS);
5204     g2D.setTransform(previousTransform);
5205   }
5206 }
5207 
5208 /**
5209  * Paints the outline of the compass when it's belongs to <code>items</code>.
5210  * @param {Graphics2D} g2D
5211  * @param {Object[]} items
5212  * @param {string|CanvasPattern} selectionOutlinePaint
5213  * @param {java.awt.BasicStroke} selectionOutlineStroke
5214  * @param {string|CanvasPattern} indicatorPaint
5215  * @param {number} planScale
5216  * @param {string} foregroundColor
5217  * @private
5218  */
5219 PlanComponent.prototype.paintCompassOutline = function(g2D, items, selectionOutlinePaint, selectionOutlineStroke, indicatorPaint, planScale, foregroundColor) {
5220   var compass = this.home.getCompass();
5221   if ((items.indexOf(compass) >= 0) 
5222       && compass.isVisible()) {
5223     var previousTransform = g2D.getTransform();
5224     g2D.translate(compass.getX(), compass.getY());
5225     g2D.rotate(compass.getNorthDirection());
5226     var diameter = compass.getDiameter();
5227     g2D.scale(diameter, diameter);
5228     
5229     g2D.setPaint(selectionOutlinePaint);
5230     g2D.setStroke(new java.awt.BasicStroke((5.5 + planScale) / diameter / planScale));
5231     g2D.draw(PlanComponent.COMPASS_DISC);
5232     g2D.setColor(foregroundColor);
5233     g2D.setStroke(new java.awt.BasicStroke(1.0 / diameter / planScale));
5234     g2D.draw(PlanComponent.COMPASS_DISC);
5235     g2D.setTransform(previousTransform);
5236     
5237     if (items.length === 1 
5238         && items[0] === compass) {
5239       g2D.setPaint(indicatorPaint);
5240       this.paintCompassIndicators(g2D, compass, indicatorPaint, planScale);
5241     }
5242   }
5243 }
5244 
5245 /**
5246  * @private
5247  */
5248 PlanComponent.prototype.paintCompassIndicators = function(g2D, compass, indicatorPaint, planScale) {
5249   if (this.resizeIndicatorVisible) {
5250     g2D.setPaint(indicatorPaint);
5251     g2D.setStroke(PlanComponent.INDICATOR_STROKE);
5252     
5253     var previousTransform = g2D.getTransform();
5254     var compassPoints = compass.getPoints();
5255     var scaleInverse = 1 / planScale;
5256     g2D.translate((compassPoints[2][0] + compassPoints[3][0]) / 2, 
5257         (compassPoints[2][1] + compassPoints[3][1]) / 2);
5258     g2D.scale(scaleInverse, scaleInverse);
5259     g2D.rotate(compass.getNorthDirection());
5260     g2D.draw(this.getIndicator(compass, PlanComponent.IndicatorType.ROTATE));
5261     g2D.setTransform(previousTransform);
5262     
5263     g2D.translate((compassPoints[1][0] + compassPoints[2][0]) / 2, 
5264         (compassPoints[1][1] + compassPoints[2][1]) / 2);
5265     g2D.scale(scaleInverse, scaleInverse);
5266     g2D.rotate(compass.getNorthDirection());
5267     g2D.draw(this.getIndicator(compass, PlanComponent.IndicatorType.RESIZE));
5268     g2D.setTransform(previousTransform);
5269   }
5270 }
5271 
5272 /**
5273  * Paints wall location feedback.
5274  * @param {Graphics2D} g2D
5275  * @param {Wall} alignedWall
5276  * @param {Level} level
5277  * @param {java.awt.geom.Point2D} locationFeedback
5278  * @param {boolean} showPointFeedback
5279  * @param {string|CanvasPattern} feedbackPaint
5280  * @param {java.awt.BasicStroke} feedbackStroke
5281  * @param {number} planScale
5282  * @param {string|CanvasPattern} pointPaint
5283  * @param {java.awt.BasicStroke} pointStroke
5284  * @private
5285  */
5286 PlanComponent.prototype.paintWallAlignmentFeedback = function(g2D, alignedWall, level, locationFeedback, showPointFeedback, feedbackPaint, feedbackStroke, planScale, pointPaint, pointStroke) {
5287   var plan = this;
5288   if (locationFeedback != null) {
5289     var margin = 0.5 / planScale;
5290     var x = locationFeedback.getX();
5291     var y = locationFeedback.getY();
5292     var deltaXToClosestWall = Infinity;
5293     var deltaYToClosestWall = Infinity;
5294     this.getViewedItems(this.home.getWalls(), level, this.otherLevelsWallsCache).forEach(function(wall) {
5295         if (wall !== alignedWall) {
5296           if (Math.abs(x - wall.getXStart()) < margin 
5297               && (alignedWall == null 
5298                   || !plan.equalsWallPoint(wall.getXStart(), wall.getYStart(), alignedWall))) {
5299             if (Math.abs(deltaYToClosestWall) > Math.abs(y - wall.getYStart())) {
5300               deltaYToClosestWall = y - wall.getYStart();
5301             }
5302           } else if (Math.abs(x - wall.getXEnd()) < margin 
5303                      && (alignedWall == null 
5304                          || !plan.equalsWallPoint(wall.getXEnd(), wall.getYEnd(), alignedWall))) {
5305             if (Math.abs(deltaYToClosestWall) > Math.abs(y - wall.getYEnd())) {
5306               deltaYToClosestWall = y - wall.getYEnd();
5307             }
5308           }
5309           
5310           if (Math.abs(y - wall.getYStart()) < margin 
5311               && (alignedWall == null 
5312                   || !plan.equalsWallPoint(wall.getXStart(), wall.getYStart(), alignedWall))) {
5313             if (Math.abs(deltaXToClosestWall) > Math.abs(x - wall.getXStart())) {
5314               deltaXToClosestWall = x - wall.getXStart();
5315             }
5316           } else if (Math.abs(y - wall.getYEnd()) < margin 
5317                      && (alignedWall == null 
5318                          || !plan.equalsWallPoint(wall.getXEnd(), wall.getYEnd(), alignedWall))) {
5319             if (Math.abs(deltaXToClosestWall) > Math.abs(x - wall.getXEnd())) {
5320               deltaXToClosestWall = x - wall.getXEnd();
5321             }
5322           }
5323           
5324           var wallPoints = wall.getPoints();
5325           wallPoints = [wallPoints[0], wallPoints[(wallPoints.length / 2 | 0) - 1], 
5326                         wallPoints[(wallPoints.length / 2 | 0)], wallPoints[wallPoints.length - 1]];
5327           for (var i = 0; i < wallPoints.length; i++) {
5328             if (Math.abs(x - wallPoints[i][0]) < margin 
5329                 && (alignedWall == null 
5330                     || !plan.equalsWallPoint(wallPoints[i][0], wallPoints[i][1], alignedWall))) {
5331               if (Math.abs(deltaYToClosestWall) > Math.abs(y - wallPoints[i][1])) {
5332                 deltaYToClosestWall = y - wallPoints[i][1];
5333               }
5334             }
5335             if (Math.abs(y - wallPoints[i][1]) < margin 
5336                 && (alignedWall == null 
5337                     || !plan.equalsWallPoint(wallPoints[i][0], wallPoints[i][1], alignedWall))) {
5338               if (Math.abs(deltaXToClosestWall) > Math.abs(x - wallPoints[i][0])) {
5339                 deltaXToClosestWall = x - wallPoints[i][0];
5340               }
5341             }
5342           }
5343         }
5344       });
5345     
5346     g2D.setPaint(feedbackPaint);
5347     g2D.setStroke(feedbackStroke);
5348     var alignmentLineOffset = this.pointerType === View.PointerType.TOUCH 
5349         ? PlanComponent.ALIGNMENT_LINE_OFFSET * 2
5350         : PlanComponent.ALIGNMENT_LINE_OFFSET;
5351     if (deltaXToClosestWall !== Infinity) {
5352       if (deltaXToClosestWall > 0) {
5353         g2D.draw(new java.awt.geom.Line2D.Float(x + alignmentLineOffset / planScale, y, 
5354             x - deltaXToClosestWall - alignmentLineOffset / planScale, y));
5355       } else {
5356         g2D.draw(new java.awt.geom.Line2D.Float(x - alignmentLineOffset / planScale, y, 
5357             x - deltaXToClosestWall + alignmentLineOffset / planScale, y));
5358       }
5359     }
5360     
5361     if (deltaYToClosestWall !== Infinity) {
5362       if (deltaYToClosestWall > 0) {
5363         g2D.draw(new java.awt.geom.Line2D.Float(x, y + alignmentLineOffset / planScale, 
5364             x, y - deltaYToClosestWall - alignmentLineOffset / planScale));
5365       } else {
5366         g2D.draw(new java.awt.geom.Line2D.Float(x, y - alignmentLineOffset / planScale, 
5367             x, y - deltaYToClosestWall + alignmentLineOffset / planScale));
5368       }
5369     }
5370     if (showPointFeedback) {
5371       this.paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
5372     }
5373   }
5374 }
5375 
5376 /**
5377  * Returns the items viewed in the plan at the given <code>level</code>.
5378  * @param {Object[]} homeItems
5379  * @param {Level} level
5380  * @param {Object[]} otherLevelItems
5381  * @return {Object[]}
5382  * @private
5383  */
5384 PlanComponent.prototype.getViewedItems = function(homeItems, level, otherLevelItems) {
5385   var viewedWalls = [];
5386   if (otherLevelItems != null) {
5387     viewedWalls.push.apply(viewedWalls, otherLevelItems);
5388   }
5389   for (var i = 0; i < homeItems.length; i++) {
5390     var wall = homeItems[i];
5391     if (this.isViewableAtLevel(wall, level)) {
5392       viewedWalls.push(wall);
5393     }
5394   }
5395   return viewedWalls;
5396 }
5397 
5398 /**
5399  * Paints point feedback.
5400  * @param {Graphics2D} g2D
5401  * @param {java.awt.geom.Point2D} locationFeedback
5402  * @param {string|CanvasPattern} feedbackPaint
5403  * @param {number} planScale
5404  * @param {string|CanvasPattern} pointPaint
5405  * @param {java.awt.BasicStroke} pointStroke
5406  * @private
5407  */
5408 PlanComponent.prototype.paintPointFeedback = function(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke) {
5409   g2D.setPaint(pointPaint);
5410   g2D.setStroke(pointStroke);
5411   var radius = this.pointerType === View.PointerType.TOUCH ? 20 :  10;
5412   var circle = new java.awt.geom.Ellipse2D.Float(locationFeedback.getX() - radius / planScale, 
5413       locationFeedback.getY() - radius / planScale, 2 * radius / planScale, 2 * radius / planScale);
5414   g2D.fill(circle);
5415   g2D.setPaint(feedbackPaint);
5416   g2D.setStroke(new java.awt.BasicStroke(1 / planScale));
5417   g2D.draw(circle);
5418   g2D.draw(new java.awt.geom.Line2D.Float(locationFeedback.getX(), 
5419       locationFeedback.getY() - radius / planScale, 
5420       locationFeedback.getX(), 
5421       locationFeedback.getY() + radius / planScale));
5422   g2D.draw(new java.awt.geom.Line2D.Float(locationFeedback.getX() - radius / planScale, 
5423       locationFeedback.getY(), 
5424       locationFeedback.getX() + radius / planScale, 
5425       locationFeedback.getY()));
5426 }
5427 
5428 /**
5429  * Returns <code>true</code> if <code>wall</code> start or end point
5430  * equals the point (<code>x</code>, <code>y</code>).
5431  * @param {number} x
5432  * @param {number} y
5433  * @param {Wall} wall
5434  * @return {boolean}
5435  * @private
5436  */
5437 PlanComponent.prototype.equalsWallPoint = function(x, y, wall) {
5438   return x === wall.getXStart() && y === wall.getYStart() 
5439          || x === wall.getXEnd() && y === wall.getYEnd();
5440 }
5441 
5442 /**
5443  * Paints room location feedback.
5444  * @param {Graphics2D} g2D
5445  * @param {Room} alignedRoom
5446  * @param {Level} level
5447  * @param {java.awt.geom.Point2D} locationFeedback
5448  * @param {boolean} showPointFeedback
5449  * @param {string|CanvasPattern} feedbackPaint
5450  * @param {java.awt.BasicStroke} feedbackStroke
5451  * @param {number} planScale
5452  * @param {string|CanvasPattern} pointPaint
5453  * @param {java.awt.BasicStroke} pointStroke
5454  * @private
5455  */
5456 PlanComponent.prototype.paintRoomAlignmentFeedback = function(g2D, alignedRoom, level, locationFeedback, showPointFeedback, feedbackPaint, feedbackStroke, planScale, pointPaint, pointStroke) {
5457   if (locationFeedback != null) {
5458     var margin = 0.5 / planScale;
5459     var x = locationFeedback.getX();
5460     var y = locationFeedback.getY();
5461     var deltaXToClosestObject = Infinity;
5462     var deltaYToClosestObject = Infinity;
5463     this.getViewedItems(this.home.getRooms(), level, this.otherLevelsRoomsCache).forEach(function(room) {
5464         var roomPoints = room.getPoints();
5465         var editedPointIndex = -1;
5466         if (room === alignedRoom) {
5467           for (var i = 0; i < roomPoints.length; i++) {
5468             if (roomPoints[i][0] === x && roomPoints[i][1] === y) {
5469               editedPointIndex = i;
5470               break;
5471             }
5472           }
5473         }
5474         for (var i = 0; i < roomPoints.length; i++) {
5475           if (editedPointIndex === -1 || (i !== editedPointIndex && roomPoints.length > 2)) {
5476             if (Math.abs(x - roomPoints[i][0]) < margin 
5477                 && Math.abs(deltaYToClosestObject) > Math.abs(y - roomPoints[i][1])) {
5478               deltaYToClosestObject = y - roomPoints[i][1];
5479             }
5480             if (Math.abs(y - roomPoints[i][1]) < margin 
5481                 && Math.abs(deltaXToClosestObject) > Math.abs(x - roomPoints[i][0])) {
5482               deltaXToClosestObject = x - roomPoints[i][0];
5483             }
5484           }
5485         }
5486       });
5487     
5488     this.getViewedItems(this.home.getWalls(), level, this.otherLevelsWallsCache).forEach(function(wall) {
5489         var wallPoints = wall.getPoints();
5490         wallPoints = [wallPoints[0], wallPoints[(wallPoints.length / 2 | 0) - 1], 
5491                       wallPoints[(wallPoints.length / 2 | 0)], wallPoints[wallPoints.length - 1]];
5492         for (var i = 0; i < wallPoints.length; i++) {
5493           if (Math.abs(x - wallPoints[i][0]) < margin 
5494               && Math.abs(deltaYToClosestObject) > Math.abs(y - wallPoints[i][1])) {
5495             deltaYToClosestObject = y - wallPoints[i][1];
5496           }
5497           if (Math.abs(y - wallPoints[i][1]) < margin 
5498               && Math.abs(deltaXToClosestObject) > Math.abs(x - wallPoints[i][0])) {
5499             deltaXToClosestObject = x - wallPoints[i][0];
5500           }
5501         }
5502       });
5503     
5504     g2D.setPaint(feedbackPaint);
5505     g2D.setStroke(feedbackStroke);
5506     var alignmentLineOffset = this.pointerType === View.PointerType.TOUCH 
5507         ? PlanComponent.ALIGNMENT_LINE_OFFSET * 2
5508         : PlanComponent.ALIGNMENT_LINE_OFFSET;
5509     if (deltaXToClosestObject !== Infinity) {
5510       if (deltaXToClosestObject > 0) {
5511         g2D.draw(new java.awt.geom.Line2D.Float(x + alignmentLineOffset / planScale, y, 
5512             x - deltaXToClosestObject - alignmentLineOffset / planScale, y));
5513       } else {
5514         g2D.draw(new java.awt.geom.Line2D.Float(x - alignmentLineOffset / planScale, y, 
5515             x - deltaXToClosestObject + alignmentLineOffset / planScale, y));
5516       }
5517     }
5518     if (deltaYToClosestObject !== Infinity) {
5519       if (deltaYToClosestObject > 0) {
5520         g2D.draw(new java.awt.geom.Line2D.Float(x, y + alignmentLineOffset / planScale, 
5521             x, y - deltaYToClosestObject - alignmentLineOffset / planScale));
5522       } else {
5523         g2D.draw(new java.awt.geom.Line2D.Float(x, y - alignmentLineOffset / planScale, 
5524             x, y - deltaYToClosestObject + alignmentLineOffset / planScale));
5525       }
5526     }
5527     if (showPointFeedback) {
5528       this.paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
5529     }
5530   }
5531 }
5532 
5533 /**
5534  * Paints dimension line location feedback.
5535  * @param {Graphics2D} g2D
5536  * @param {DimensionLine} alignedDimensionLine
5537  * @param {Level} level
5538  * @param {java.awt.geom.Point2D} locationFeedback
5539  * @param {boolean} showPointFeedback
5540  * @param {string|CanvasPattern} feedbackPaint
5541  * @param {java.awt.BasicStroke} feedbackStroke
5542  * @param {number} planScale
5543  * @param {string|CanvasPattern} pointPaint
5544  * @param {java.awt.BasicStroke} pointStroke
5545  * @private
5546  */
5547 PlanComponent.prototype.paintDimensionLineAlignmentFeedback = function(g2D, alignedDimensionLine, level, locationFeedback, showPointFeedback, feedbackPaint, feedbackStroke, planScale, pointPaint, pointStroke) {
5548   var plan = this;
5549   if (locationFeedback != null) {
5550     var margin = 0.5 / planScale;
5551     var x = locationFeedback.getX();
5552     var y = locationFeedback.getY();
5553     var deltaXToClosestObject = Infinity;
5554     var deltaYToClosestObject = Infinity;
5555     this.getViewedItems(this.home.getRooms(), level, this.otherLevelsRoomsCache).forEach(function(room) {
5556         var roomPoints = room.getPoints();
5557         for (var i = 0; i < roomPoints.length; i++) {
5558           if (Math.abs(x - roomPoints[i][0]) < margin 
5559               && Math.abs(deltaYToClosestObject) > Math.abs(y - roomPoints[i][1])) {
5560             deltaYToClosestObject = y - roomPoints[i][1];
5561           }
5562           if (Math.abs(y - roomPoints[i][1]) < margin 
5563               && Math.abs(deltaXToClosestObject) > Math.abs(x - roomPoints[i][0])) {
5564             deltaXToClosestObject = x - roomPoints[i][0];
5565           }
5566         }
5567       });
5568     
5569     this.home.getDimensionLines().forEach(function(dimensionLine) {
5570         if (plan.isViewableAtLevel(dimensionLine, level) 
5571             && dimensionLine !== alignedDimensionLine) {
5572           if (Math.abs(x - dimensionLine.getXStart()) < margin 
5573               && (alignedDimensionLine == null 
5574                   || !plan.equalsDimensionLinePoint(dimensionLine.getXStart(), dimensionLine.getYStart(), 
5575                           alignedDimensionLine))) {
5576             if (Math.abs(deltaYToClosestObject) > Math.abs(y - dimensionLine.getYStart())) {
5577               deltaYToClosestObject = y - dimensionLine.getYStart();
5578             }
5579           } else if (Math.abs(x - dimensionLine.getXEnd()) < margin 
5580                      && (alignedDimensionLine == null 
5581                          || !plan.equalsDimensionLinePoint(dimensionLine.getXEnd(), dimensionLine.getYEnd(), 
5582                                  alignedDimensionLine))) {
5583             if (Math.abs(deltaYToClosestObject) > Math.abs(y - dimensionLine.getYEnd())) {
5584               deltaYToClosestObject = y - dimensionLine.getYEnd();
5585             }
5586           }
5587           if (Math.abs(y - dimensionLine.getYStart()) < margin 
5588               && (alignedDimensionLine == null 
5589                   || !plan.equalsDimensionLinePoint(dimensionLine.getXStart(), dimensionLine.getYStart(), 
5590                           alignedDimensionLine))) {
5591             if (Math.abs(deltaXToClosestObject) > Math.abs(x - dimensionLine.getXStart())) {
5592               deltaXToClosestObject = x - dimensionLine.getXStart();
5593             }
5594           } else if (Math.abs(y - dimensionLine.getYEnd()) < margin 
5595                      && (alignedDimensionLine == null 
5596                          || !plan.equalsDimensionLinePoint(dimensionLine.getXEnd(), dimensionLine.getYEnd(), 
5597                                  alignedDimensionLine))) {
5598             if (Math.abs(deltaXToClosestObject) > Math.abs(x - dimensionLine.getXEnd())) {
5599               deltaXToClosestObject = x - dimensionLine.getXEnd();
5600             }
5601           }
5602         }
5603       });
5604     
5605     this.getViewedItems(this.home.getWalls(), level, this.otherLevelsWallsCache).forEach(function(wall) {
5606         var wallPoints = wall.getPoints();
5607         wallPoints = [wallPoints[0], wallPoints[(wallPoints.length / 2 | 0) - 1], 
5608                       wallPoints[(wallPoints.length / 2 | 0)], wallPoints[wallPoints.length - 1]];
5609         for (var i = 0; i < wallPoints.length; i++) {
5610           if (Math.abs(x - wallPoints[i][0]) < margin 
5611               && Math.abs(deltaYToClosestObject) > Math.abs(y - wallPoints[i][1])) {
5612             deltaYToClosestObject = y - wallPoints[i][1];
5613           }
5614           if (Math.abs(y - wallPoints[i][1]) < margin 
5615               && Math.abs(deltaXToClosestObject) > Math.abs(x - wallPoints[i][0])) {
5616             deltaXToClosestObject = x - wallPoints[i][0];
5617           }
5618         }
5619       });
5620     
5621     this.home.getFurniture().forEach(function(piece) {
5622         if (piece.isVisible() 
5623             && plan.isViewableAtLevel(piece, level)) {
5624           var piecePoints = piece.getPoints();
5625           for (var i = 0; i < piecePoints.length; i++) {
5626             if (Math.abs(x - piecePoints[i][0]) < margin 
5627                 && Math.abs(deltaYToClosestObject) > Math.abs(y - piecePoints[i][1])) {
5628               deltaYToClosestObject = y - piecePoints[i][1];
5629             }
5630             if (Math.abs(y - piecePoints[i][1]) < margin 
5631                 && Math.abs(deltaXToClosestObject) > Math.abs(x - piecePoints[i][0])) {
5632               deltaXToClosestObject = x - piecePoints[i][0];
5633             }
5634           }
5635         }
5636       });
5637     
5638     g2D.setPaint(feedbackPaint);
5639     g2D.setStroke(feedbackStroke);
5640     var alignmentLineOffset = this.pointerType === View.PointerType.TOUCH 
5641         ? PlanComponent.ALIGNMENT_LINE_OFFSET * 2
5642         : PlanComponent.ALIGNMENT_LINE_OFFSET;
5643     if (deltaXToClosestObject !== Infinity) {
5644       if (deltaXToClosestObject > 0) {
5645         g2D.draw(new java.awt.geom.Line2D.Float(x + alignmentLineOffset / planScale, y, 
5646             x - deltaXToClosestObject - alignmentLineOffset / planScale, y));
5647       } else {
5648         g2D.draw(new java.awt.geom.Line2D.Float(x - alignmentLineOffset / planScale, y, 
5649             x - deltaXToClosestObject + alignmentLineOffset / planScale, y));
5650       }
5651     }
5652     if (deltaYToClosestObject !== Infinity) {
5653       if (deltaYToClosestObject > 0) {
5654         g2D.draw(new java.awt.geom.Line2D.Float(x, y + alignmentLineOffset / planScale, 
5655             x, y - deltaYToClosestObject - alignmentLineOffset / planScale));
5656       } else {
5657         g2D.draw(new java.awt.geom.Line2D.Float(x, y - alignmentLineOffset / planScale, 
5658             x, y - deltaYToClosestObject + alignmentLineOffset / planScale));
5659       }
5660     }
5661     if (showPointFeedback) {
5662       this.paintPointFeedback(g2D, locationFeedback, feedbackPaint, planScale, pointPaint, pointStroke);
5663     }
5664   }
5665 }
5666 
5667 /**
5668  * Returns <code>true</code> if <code>dimensionLine</code> start or end point
5669  * equals the point (<code>x</code>, <code>y</code>).
5670  * @param {number} x
5671  * @param {number} y
5672  * @param {DimensionLine} dimensionLine
5673  * @return {boolean}
5674  * @private
5675  */
5676 PlanComponent.prototype.equalsDimensionLinePoint = function(x, y, dimensionLine) {
5677   return x === dimensionLine.getXStart() && y === dimensionLine.getYStart() 
5678          || x === dimensionLine.getXEnd() && y === dimensionLine.getYEnd();
5679 }
5680 
5681 /**
5682  * Paints an arc centered at <code>center</code> point that goes
5683  * @param {Graphics2D} g2D
5684  * @param {java.awt.geom.Point2D} center
5685  * @param {java.awt.geom.Point2D} point1
5686  * @param {java.awt.geom.Point2D} point2
5687  * @param {number} planScale
5688  * @param {string} selectionColor
5689  * @private
5690  */
5691 PlanComponent.prototype.paintAngleFeedback = function(g2D, center, point1, point2, planScale, selectionColor) {
5692   if (!point1.equals(center) && !point2.equals(center)) {
5693     g2D.setColor(selectionColor);
5694     g2D.setStroke(new java.awt.BasicStroke(1 / planScale));
5695     var angle1 = Math.atan2(center.getY() - point1.getY(), point1.getX() - center.getX());
5696     if (angle1 < 0) {
5697       angle1 = 2 * Math.PI + angle1;
5698     }
5699     var angle2 = Math.atan2(center.getY() - point2.getY(), point2.getX() - center.getX());
5700     if (angle2 < 0) {
5701       angle2 = 2 * Math.PI + angle2;
5702     }
5703     var extent = angle2 - angle1;
5704     if (angle1 > angle2) {
5705       extent = 2 * Math.PI + extent;
5706     }
5707     var previousTransform = g2D.getTransform();
5708     g2D.translate(center.getX(), center.getY());
5709     var radius = 20 / planScale;
5710     g2D.draw(new java.awt.geom.Arc2D.Double(-radius, -radius, 
5711         radius * 2, radius * 2, angle1 * 180 / Math.PI, extent * 180 / Math.PI, java.awt.geom.Arc2D.OPEN));
5712     radius += 5 / planScale;
5713     g2D.draw(new java.awt.geom.Line2D.Double(0, 0, radius * Math.cos(angle1), -radius * Math.sin(angle1)));
5714     g2D.draw(new java.awt.geom.Line2D.Double(0, 0, radius * Math.cos(angle1 + extent), -radius * Math.sin(angle1 + extent)));
5715     g2D.setTransform(previousTransform);
5716   }
5717 }
5718 
5719 /**
5720  * Paints the observer camera at its current location, if home camera is the observer camera.
5721  * @param {Graphics2D} g2D
5722  * @param {Object[]} selectedItems
5723  * @param {string|CanvasPattern} selectionOutlinePaint
5724  * @param {java.awt.Stroke} selectionOutlineStroke
5725  * @param {string|CanvasPattern} indicatorPaint
5726  * @param {number} planScale
5727  * @param {string} backgroundColor
5728  * @param {string} foregroundColor
5729  * @private
5730  */
5731 PlanComponent.prototype.paintCamera = function(g2D, selectedItems, selectionOutlinePaint, selectionOutlineStroke, indicatorPaint, planScale, backgroundColor, foregroundColor) {
5732   var camera = this.home.getObserverCamera();
5733   if (camera === this.home.getCamera()) {
5734     var previousTransform = g2D.getTransform();
5735     g2D.translate(camera.getX(), camera.getY());
5736     g2D.rotate(camera.getYaw());
5737     
5738     var points = camera.getPoints();
5739     var yScale = java.awt.geom.Point2D.distance(points[0][0], points[0][1], points[3][0], points[3][1]);
5740     var xScale = java.awt.geom.Point2D.distance(points[0][0], points[0][1], points[1][0], points[1][1]);
5741     var cameraTransform = java.awt.geom.AffineTransform.getScaleInstance(xScale, yScale);
5742     var cameraScale = camera.getPlanScale();
5743     var scaledCameraBody = new java.awt.geom.Area(cameraScale <= 1 ? PlanComponent.CAMERA_HUMAN_BODY : PlanComponent.CAMERA_BODY).createTransformedArea(cameraTransform);
5744     var scaledCameraHead = new java.awt.geom.Area(cameraScale <= 1 ? PlanComponent.CAMERA_HUMAN_HEAD : PlanComponent.CAMERA_BUTTON).createTransformedArea(cameraTransform);
5745     
5746     g2D.setPaint(backgroundColor);
5747     g2D.fill(scaledCameraBody);
5748     g2D.setPaint(foregroundColor);
5749     var stroke = new java.awt.BasicStroke(this.getStrokeWidth(ObserverCamera, PlanComponent.PaintMode.PAINT) / planScale);
5750     g2D.setStroke(stroke);
5751     g2D.draw(scaledCameraBody);
5752     
5753     if (selectedItems.indexOf(camera) >= 0 
5754         && this.selectedItemsOutlinePainted) {
5755       g2D.setPaint(selectionOutlinePaint);
5756       g2D.setStroke(selectionOutlineStroke);
5757       var cameraOutline = new java.awt.geom.Area(scaledCameraBody);
5758       cameraOutline.add(new java.awt.geom.Area(scaledCameraHead));
5759       g2D.draw(cameraOutline);
5760     }
5761     
5762     g2D.setPaint(backgroundColor);
5763     g2D.fill(scaledCameraHead);
5764     g2D.setPaint(foregroundColor);
5765     g2D.setStroke(stroke);
5766     g2D.draw(scaledCameraHead);
5767     var sin = Math.sin(camera.getFieldOfView() / 2);
5768     var cos = Math.cos(camera.getFieldOfView() / 2);
5769     var xStartAngle = (0.9 * yScale * sin);
5770     var yStartAngle = (0.9 * yScale * cos);
5771     var xEndAngle = (2.2 * yScale * sin);
5772     var yEndAngle = (2.2 * yScale * cos);
5773     var cameraFieldOfViewAngle = new java.awt.geom.GeneralPath();
5774     cameraFieldOfViewAngle.moveTo(xStartAngle, yStartAngle);
5775     cameraFieldOfViewAngle.lineTo(xEndAngle, yEndAngle);
5776     cameraFieldOfViewAngle.moveTo(-xStartAngle, yStartAngle);
5777     cameraFieldOfViewAngle.lineTo(-xEndAngle, yEndAngle);
5778     g2D.draw(cameraFieldOfViewAngle);
5779     g2D.setTransform(previousTransform);
5780     
5781     if (selectedItems.length === 1 
5782         && selectedItems[0] === camera) {
5783       this.paintCameraRotationIndicators(g2D, camera, indicatorPaint, planScale);
5784     }
5785   }
5786 }
5787 
5788 /**
5789  * @private
5790  */
5791 PlanComponent.prototype.paintCameraRotationIndicators = function(g2D, camera, indicatorPaint, planScale) {
5792   if (this.resizeIndicatorVisible) {
5793     g2D.setPaint(indicatorPaint);
5794     g2D.setStroke(PlanComponent.INDICATOR_STROKE);
5795     
5796     var previousTransform = g2D.getTransform();
5797     var cameraPoints = camera.getPoints();
5798     var scaleInverse = 1 / planScale;
5799     g2D.translate((cameraPoints[0][0] + cameraPoints[3][0]) / 2, 
5800         (cameraPoints[0][1] + cameraPoints[3][1]) / 2);
5801     g2D.scale(scaleInverse, scaleInverse);
5802     g2D.rotate(camera.getYaw());
5803     g2D.draw(this.getIndicator(camera, PlanComponent.IndicatorType.ROTATE));
5804     g2D.setTransform(previousTransform);
5805     
5806     g2D.translate((cameraPoints[1][0] + cameraPoints[2][0]) / 2, 
5807         (cameraPoints[1][1] + cameraPoints[2][1]) / 2);
5808     g2D.scale(scaleInverse, scaleInverse);
5809     g2D.rotate(camera.getYaw());
5810     g2D.draw(this.getIndicator(camera, PlanComponent.IndicatorType.ROTATE_PITCH));
5811     g2D.setTransform(previousTransform);
5812     
5813     var elevationIndicator = this.getIndicator(camera, PlanComponent.IndicatorType.ELEVATE);
5814     if (elevationIndicator != null) {
5815       g2D.translate((cameraPoints[0][0] + cameraPoints[1][0]) / 2, 
5816           (cameraPoints[0][1] + cameraPoints[1][1]) / 2);
5817       g2D.scale(scaleInverse, scaleInverse);
5818       g2D.draw(PlanComponent.POINT_INDICATOR);
5819       g2D.translate(Math.sin(camera.getYaw()) * 8, -Math.cos(camera.getYaw()) * 8);
5820       g2D.draw(elevationIndicator);
5821       g2D.setTransform(previousTransform);
5822     }
5823   }
5824 }
5825 
5826 /**
5827  * Paints rectangle feedback.
5828  * @param {Graphics2D} g2D
5829  * @param {string} selectionColor
5830  * @param {number} planScale
5831  * @private
5832  */
5833 PlanComponent.prototype.paintRectangleFeedback = function(g2D, selectionColor, planScale) {
5834   if (this.rectangleFeedback != null) {
5835     g2D.setPaint(ColorTools.toRGBAStyle(selectionColor, 0.125));
5836     g2D.fill(this.rectangleFeedback);
5837     g2D.setPaint(selectionColor);
5838     g2D.setStroke(new java.awt.BasicStroke(1 / planScale));
5839     g2D.draw(this.rectangleFeedback);
5840   }
5841 }
5842 
5843 /**
5844  * Sets rectangle selection feedback coordinates.
5845  * @param {number} x0
5846  * @param {number} y0
5847  * @param {number} x1
5848  * @param {number} y1
5849  */
5850 PlanComponent.prototype.setRectangleFeedback = function(x0, y0, x1, y1) {
5851   this.rectangleFeedback = new java.awt.geom.Rectangle2D.Float(x0, y0, 0, 0);
5852   this.rectangleFeedback.add(x1, y1);
5853   this.repaint();
5854 }
5855 
5856 /**
5857  * Ensures selected items are visible at screen and moves
5858  * scroll bars if needed.
5859  */
5860 PlanComponent.prototype.makeSelectionVisible = function() {
5861   if (this.isScrolled() && !this.selectionScrollUpdated) {
5862     this.selectionScrollUpdated = true;
5863     var plan = this;
5864     setTimeout(function() {
5865         plan.selectionScrollUpdated = false;
5866         var selectionBounds = plan.getSelectionBounds(true);
5867         if (selectionBounds != null) {
5868           var pixelBounds = plan.getShapePixelBounds(selectionBounds);
5869           pixelBounds = new java.awt.geom.Rectangle2D.Float(pixelBounds.getX() - 5, pixelBounds.getY() - 5, 
5870               pixelBounds.getWidth() + 10, pixelBounds.getHeight() + 10);
5871           var visibleRectangle = new java.awt.geom.Rectangle2D.Float(0, 0, 
5872               plan.scrollPane.clientWidth, plan.scrollPane.clientHeight);
5873           if (!pixelBounds.intersects(visibleRectangle)) {
5874             plan.scrollRectToVisible(pixelBounds);
5875           }
5876         }
5877       });
5878   }
5879 }
5880 
5881 /** 
5882  * @private 
5883  */
5884 PlanComponent.prototype.scrollRectToVisible = function(rectangle) {
5885   if (this.isScrolled()) {
5886     var dx = 0;
5887     var dy = 0;
5888     if (rectangle.x < 0) {
5889       dx = rectangle.x;
5890     } else if (rectangle.getX() + rectangle.getWidth() > this.scrollPane.clientWidth) {
5891       dx = rectangle.getX() + rectangle.getWidth() - this.scrollPane.clientWidth;
5892     }
5893     if (rectangle.y < 0) {
5894       dy = rectangle.y;
5895     } else if (rectangle.getY() + rectangle.getHeight() > this.scrollPane.clientHeight) {
5896       dy = rectangle.getY() + rectangle.getHeight() - this.scrollPane.clientHeight;
5897     }
5898     this.moveView(this.convertPixelToLength(dx), this.convertPixelToLength(dy));
5899   }
5900 }
5901 
5902 /**
5903  * Returns the bounds of the selected items.
5904  * @param {boolean} includeCamera
5905  * @return {java.awt.geom.Rectangle2D}
5906  * @private
5907  */
5908 PlanComponent.prototype.getSelectionBounds = function(includeCamera) {
5909   var g = this.getGraphics();
5910   if (g != null) {
5911     this.setRenderingHints(g);
5912   }
5913   if (includeCamera) {
5914     return this.getItemsBounds(g, this.home.getSelectedItems());
5915   } else {
5916     var selectedItems = this.home.getSelectedItems().slice(0);
5917     var index = selectedItems.indexOf(this.home.getCamera());
5918     if (index >= 0) {
5919       selectedItems.splice(index, 1);
5920     }
5921     return this.getItemsBounds(g, selectedItems);
5922   }
5923 }
5924 
5925 /**
5926  * Ensures the point at (<code>x</code>, <code>y</code>) is visible,
5927  * moving scroll bars if needed.
5928  * @param {number} x
5929  * @param {number} y
5930  */
5931 PlanComponent.prototype.makePointVisible = function(x, y) {
5932   this.scrollRectToVisible(this.getShapePixelBounds(
5933       new java.awt.geom.Rectangle2D.Float(x, y, this.getPixelLength(), this.getPixelLength())));
5934 }
5935 
5936 /**
5937  * Moves the view from (dx, dy) unit in the scrolling zone it belongs to.
5938  * @param {number} dx
5939  * @param {number} dy
5940  */
5941 PlanComponent.prototype.moveView = function(dx, dy) {
5942   if (this.isScrolled() 
5943       && (dx != 0 || dy != 0)) {
5944     this.scrollPane.scrollLeft += this.convertLengthToPixel(dx);
5945     this.scrollPane.scrollTop += this.convertLengthToPixel(dy);
5946     this.repaint();
5947   }
5948 }
5949 
5950 /**
5951  * Returns the scale used to display the plan.
5952  * @return {number}
5953  */
5954 PlanComponent.prototype.getScale = function() {
5955   return this.scale;
5956 }
5957 
5958 /**
5959  * Sets the scale used to display the plan.
5960  * If this component is displayed in a scrolled panel the view position is updated
5961  * to ensure the center's view will remain the same after the scale change.
5962  * @param {number} scale
5963  */
5964 PlanComponent.prototype.setScale = function(scale) {
5965   if (this.scale !== scale) {
5966     var xViewCenterPosition = 0;
5967     var yViewCenterPosition = 0;
5968     if (this.isScrolled()) {
5969       xViewCenterPosition = this.convertXPixelToModel(this.scrollPane.clientWidth / 2);
5970       yViewCenterPosition = this.convertYPixelToModel(this.scrollPane.clientHeight / 2);
5971     }
5972 
5973     this.scale = scale;
5974     this.revalidate();
5975     
5976     if (this.isScrolled()
5977         && !isNaN(xViewCenterPosition)) {
5978       var viewWidth = this.convertPixelToLength(this.scrollPane.clientWidth);
5979       var viewHeight = this.convertPixelToLength(this.scrollPane.clientHeight);
5980       this.scrollPane.scrollLeft += this.convertXModelToPixel(xViewCenterPosition - viewWidth / 2);
5981       this.scrollPane.scrollTop += this.convertYModelToPixel(yViewCenterPosition - viewHeight / 2);
5982     }
5983   }
5984 }
5985 
5986 /**
5987  * Returns <code>x</code> converted in model coordinates space.
5988  * @param {number} x
5989  * @return {number}
5990  */
5991 PlanComponent.prototype.convertXPixelToModel = function(x) {
5992   var insets = this.getInsets();
5993   var planBounds = this.getPlanBounds();
5994   return this.convertPixelToLength(x - insets.left + (this.isScrolled() ? this.scrollPane.scrollLeft : 0)) - PlanComponent.MARGIN + planBounds.getMinX();
5995 }
5996 
5997 /**
5998  * Returns <code>y</code> converted in model coordinates space.
5999  * @param {number} y
6000  * @return {number}
6001  */
6002 PlanComponent.prototype.convertYPixelToModel = function(y) {
6003   var insets = this.getInsets();
6004   var planBounds = this.getPlanBounds();
6005   return this.convertPixelToLength(y - insets.top + (this.isScrolled() ? this.scrollPane.scrollTop : 0)) - PlanComponent.MARGIN + planBounds.getMinY();
6006 }
6007 
6008 /**
6009  * Returns the length in model units (cm) of the given <code>size</code> in pixels.
6010  * @private
6011  */
6012 PlanComponent.prototype.convertPixelToLength = function(size) {
6013   return size * this.getPixelLength();
6014 }
6015 
6016 /**
6017  * Returns <code>x</code> converted in view coordinates space.
6018  * @param {number} x
6019  * @return {number}
6020  * @private
6021  */
6022 PlanComponent.prototype.convertXModelToPixel = function(x) {
6023   var insets = this.getInsets();
6024   var planBounds = this.getPlanBounds();
6025   return this.convertLengthToPixel(x - planBounds.getMinX() + PlanComponent.MARGIN) + insets.left - (this.isScrolled() ? this.scrollPane.scrollLeft : 0);
6026 }
6027 
6028 /**
6029  * Returns <code>y</code> converted in view coordinates space.
6030  * @param {number} y
6031  * @return {number}
6032  * @private
6033  */
6034 PlanComponent.prototype.convertYModelToPixel = function(y) {
6035   var insets = this.getInsets();
6036   var planBounds = this.getPlanBounds();
6037   return this.convertLengthToPixel(y - planBounds.getMinY() + PlanComponent.MARGIN) + insets.top - (this.isScrolled() ? this.scrollPane.scrollTop : 0);
6038 }
6039 
6040 /**
6041  * Returns the size in pixels of the given <code>length</code> in model units (cm).
6042  * @private
6043  */
6044 PlanComponent.prototype.convertLengthToPixel = function(length) {
6045   return Math.round(length / this.getPixelLength());
6046 }
6047 
6048 /**
6049  * Returns <code>x</code> converted in screen coordinates space.
6050  * @param {number} x
6051  * @return {number}
6052  */
6053 PlanComponent.prototype.convertXModelToScreen = function(x) {
6054   return this.canvas.getBoundingClientRect().left + this.convertXModelToPixel(x);
6055 }
6056 
6057 /**
6058  * Returns <code>y</code> converted in screen coordinates space.
6059  * @param {number} y
6060  * @return {number}
6061  */
6062 PlanComponent.prototype.convertYModelToScreen = function(y) {
6063   return this.canvas.getBoundingClientRect().top + this.convertYModelToPixel(y);
6064 }
6065 
6066 /**
6067  * Returns the length in centimeters of a pixel with the current scale.
6068  * @return {number}
6069  */
6070 PlanComponent.prototype.getPixelLength = function() {
6071   // On contrary to Java version based on resolution scale, we use the actual scale 
6072   return 1 / this.getScale();
6073 }
6074 
6075 /**
6076  * Returns the bounds of <code>shape</code> in pixels coordinates space.
6077  * @param {Object} shape
6078  * @return {java.awt.geom.Rectangle2D.Float}
6079  * @private
6080  */
6081 PlanComponent.prototype.getShapePixelBounds = function(shape) {
6082   var shapeBounds = shape.getBounds2D();
6083   return new java.awt.geom.Rectangle2D.Float(
6084       this.convertXModelToPixel(shapeBounds.getMinX()), 
6085       this.convertYModelToPixel(shapeBounds.getMinY()), 
6086       this.convertLengthToPixel(shapeBounds.getWidth()), 
6087       this.convertLengthToPixel(shapeBounds.getHeight()));
6088 }
6089 
6090 /**
6091  * Sets the cursor of this component.
6092  * @param {PlanView.CursorType|string} cursorType
6093  */
6094 PlanComponent.prototype.setCursor = function(cursorType) {
6095   if (typeof cursorType === 'string') {
6096     this.canvas.style.cursor = cursorType;
6097   } else {
6098     switch (cursorType) {
6099       case PlanView.CursorType.ROTATION:
6100       case PlanView.CursorType.HEIGHT:
6101       case PlanView.CursorType.POWER:
6102       case PlanView.CursorType.ELEVATION:
6103       case PlanView.CursorType.RESIZE:
6104         if (this.mouseListener.longTouch != null) {
6105           clearTimeout(this.mouseListener.longTouch);
6106           this.mouseListener.longTouch = null;
6107           this.stopLongTouchAnimation();
6108         }
6109         // No break;
6110       case PlanView.CursorType.MOVE:
6111         if (this.lastTouchX 
6112             && this.lastTouchY) {
6113           this.startIndicatorAnimation(this.lastTouchX, this.lastTouchY, PlanView.CursorType[cursorType].toLowerCase());
6114         }
6115         break;
6116     }
6117 
6118     switch (cursorType) {
6119       case PlanView.CursorType.DRAW:
6120         this.setCursor('crosshair');
6121         break;
6122       case PlanView.CursorType.ROTATION:
6123         this.setCursor(this.rotationCursor);
6124         break;
6125       case PlanView.CursorType.HEIGHT:
6126         this.setCursor(this.heightCursor);
6127         break;
6128       case PlanView.CursorType.POWER:
6129         this.setCursor(this.powerCursor);
6130         break;
6131       case PlanView.CursorType.ELEVATION:
6132         this.setCursor(this.elevationCursor);
6133         break;
6134       case PlanView.CursorType.RESIZE:
6135         this.setCursor(this.resizeCursor);
6136         break;
6137       case PlanView.CursorType.PANNING:
6138         this.setCursor(this.panningCursor);
6139         break;
6140       case PlanView.CursorType.DUPLICATION:
6141         this.setCursor(this.duplicationCursor);
6142         break;
6143       case PlanView.CursorType.MOVE:
6144         this.setCursor(this.moveCursor);
6145         break;
6146       case PlanView.CursorType.SELECTION:
6147       default:
6148         this.setCursor('default');
6149         break;
6150     }
6151   }
6152 }
6153 
6154 /**
6155  * Sets tool tip text displayed as feedback.
6156  * @param {string} toolTipFeedback the text displayed in the tool tip
6157  *                    or <code>null</code> to make tool tip disappear.
6158  */
6159 PlanComponent.prototype.setToolTipFeedback = function(toolTipFeedback, x, y) {
6160   this.tooltip.style.width = "";
6161   this.tooltip.style.marginLeft = "";
6162   if (toolTipFeedback.indexOf("<html>") === 0) {
6163     this.tooltip.style.textAlign = "left";
6164   } else {
6165     this.tooltip.style.textAlign = "center";
6166   }
6167   this.tooltip.innerHTML = toolTipFeedback.replace("<html>", "").replace("</html>", "");
6168   var marginTop = -(this.tooltip.clientHeight + (this.pointerType === View.PointerType.TOUCH ? 55 : 20));
6169   this.tooltip.style.marginTop = (marginTop - (this.pointerType === View.PointerType.TOUCH && this.touchOverlay.style.visibility == "visible" ? 15 : 0)) + "px";
6170   var width = this.tooltip.clientWidth + 10;
6171   this.tooltip.style.width = width + "px";
6172   this.tooltip.style.marginLeft = -width / 2 + "px";
6173   var containerRect = this.container.getBoundingClientRect();
6174   this.tooltip.style.left = Math.max(5 + width / 2, 
6175      Math.min(window.innerWidth - width / 2 - 10, containerRect.left + this.convertXModelToPixel(x))) + "px";
6176   var top =  containerRect.top + this.convertYModelToPixel(y);
6177   this.tooltip.style.top =  (top + marginTop > 15 ? top : top - marginTop + (this.pointerType === View.PointerType.TOUCH ? 100 : 20)) + "px";
6178   this.tooltip.style.visibility = "visible";  
6179 }
6180 
6181 /**
6182  * Set tool tip edition.
6183  * @param {Array} toolTipEditedProperties
6184  * @param {Array} toolTipPropertyValues
6185  * @param {number} x
6186  * @param {number} y
6187  */
6188 PlanComponent.prototype.setToolTipEditedProperties = function(toolTipEditedProperties, toolTipPropertyValues, x, y) {
6189   // TODO
6190 }
6191 
6192 /**
6193  * Deletes tool tip text from screen.
6194  */
6195 PlanComponent.prototype.deleteToolTipFeedback = function() {
6196   this.tooltip.style.visibility = "hidden";
6197   this.tooltip.style.width = "";
6198   this.tooltip.style.marginLeft = "";
6199   this.tooltip.style.marginTop = "";
6200 }
6201 
6202 /**
6203  * Sets whether the resize indicator of selected wall or piece of furniture
6204  * should be visible or not.
6205  * @param {boolean} resizeIndicatorVisible
6206  */
6207 PlanComponent.prototype.setResizeIndicatorVisible = function(resizeIndicatorVisible) {
6208   this.resizeIndicatorVisible = resizeIndicatorVisible;
6209   this.repaint();
6210 }
6211 
6212 /**
6213  * Sets the location point for alignment feedback.
6214  * @param {Object} alignedObjectClass
6215  * @param {Object} alignedObject
6216  * @param {number} x
6217  * @param {number} y
6218  * @param {boolean} showPointFeedback
6219  */
6220 PlanComponent.prototype.setAlignmentFeedback = function(alignedObjectClass, alignedObject, x, y, showPointFeedback) {
6221   this.alignedObjectClass = alignedObjectClass;
6222   this.alignedObjectFeedback = alignedObject;
6223   this.locationFeeback = new java.awt.geom.Point2D.Float(x, y);
6224   this.showPointFeedback = showPointFeedback;
6225   this.repaint();
6226 }
6227 
6228 /**
6229  * Sets the points used to draw an angle in plan view.
6230  * @param {number} xCenter
6231  * @param {number} yCenter
6232  * @param {number} x1
6233  * @param {number} y1
6234  * @param {number} x2
6235  * @param {number} y2
6236  */
6237 PlanComponent.prototype.setAngleFeedback = function(xCenter, yCenter, x1, y1, x2, y2) {
6238   this.centerAngleFeedback = new java.awt.geom.Point2D.Float(xCenter, yCenter);
6239   this.point1AngleFeedback = new java.awt.geom.Point2D.Float(x1, y1);
6240   this.point2AngleFeedback = new java.awt.geom.Point2D.Float(x2, y2);
6241 }
6242 
6243 /**
6244  * Sets the feedback of dragged items drawn during a drag and drop operation,
6245  * initiated from outside of plan view.
6246  * @param {Object[]} draggedItems
6247  */
6248 PlanComponent.prototype.setDraggedItemsFeedback = function(draggedItems) {
6249   this.draggedItemsFeedback = draggedItems;
6250   this.repaint();
6251 }
6252 
6253 /**
6254  * Sets the given dimension lines to be drawn as feedback.
6255  * @param {DimensionLine[]} dimensionLines
6256  */
6257 PlanComponent.prototype.setDimensionLinesFeedback = function(dimensionLines) {
6258   this.dimensionLinesFeedback = dimensionLines;
6259   this.repaint();
6260 }
6261 
6262 /**
6263  * Deletes all elements shown as feedback.
6264  */
6265 PlanComponent.prototype.deleteFeedback = function() {
6266   this.deleteToolTipFeedback();
6267   this.rectangleFeedback = null;
6268   this.alignedObjectClass = null;
6269   this.alignedObjectFeedback = null;
6270   this.locationFeeback = null;
6271   this.centerAngleFeedback = null;
6272   this.point1AngleFeedback = null;
6273   this.point2AngleFeedback = null;
6274   this.draggedItemsFeedback = null;
6275   this.dimensionLinesFeedback = null;
6276   this.repaint();
6277 }
6278 
6279 /**
6280  * Returns <code>true</code>.
6281  * @param {Object[]} items
6282  * @param {number} x
6283  * @param {number} y
6284  * @return {boolean}
6285  */
6286 PlanComponent.prototype.canImportDraggedItems = function(items, x, y) {
6287   return true;
6288 }
6289 
6290 /**
6291  * Returns the size of the given piece of furniture in the horizontal plan,
6292  * or <code>null</code> if the view isn't able to compute such a value.
6293  * @param {HomePieceOfFurniture} piece
6294  * @return {Array}
6295  */
6296 PlanComponent.prototype.getPieceOfFurnitureSizeInPlan = function(piece) {
6297   if (piece.getRoll() === 0 && piece.getPitch() === 0) {
6298     return [piece.getWidth(), piece.getDepth(), piece.getHeight()];
6299   }
6300   else if (!this.isFurnitureSizeInPlanSupported()) {
6301     return null;
6302   }
6303   else {
6304     return PlanComponent.PieceOfFurnitureModelIcon.computePieceOfFurnitureSizeInPlan(piece, this.object3dFactory);
6305   }
6306 }
6307 
6308 /**
6309  * Returns <code>true</code> if this component is able to compute the size of horizontally rotated furniture.
6310  * @return {boolean}
6311  */
6312 PlanComponent.prototype.isFurnitureSizeInPlanSupported = function() {
6313   return PlanComponent.WEBGL_AVAILABLE;
6314 }
6315 
6316 /**
6317  * Removes components added to this pane and their listeners.
6318  */
6319 PlanComponent.prototype.dispose = function() {
6320   this.container.removeEventListener("keydown", this.keyDownListener, false);
6321   this.container.removeEventListener("keyup", this.keyUpListener, false);
6322   this.container.removeEventListener("focusout", this.focusOutListener);
6323   if (OperatingSystem.isInternetExplorerOrLegacyEdge()
6324       && window.PointerEvent) {
6325     window.removeEventListener("pointermove", this.mouseListener.windowPointerMoved);
6326     window.removeEventListener("pointerup", this.mouseListener.windowPointerReleased);
6327   } else {
6328     window.removeEventListener("mousemove", this.mouseListener.windowMouseMoved);
6329     window.removeEventListener("mouseup", this.mouseListener.windowMouseReleased);
6330   }
6331   document.body.removeChild(this.touchOverlay);
6332   document.body.removeChild(this.tooltip);
6333   window.removeEventListener("resize", this.windowResizeListener);
6334   if (this.scrollPane != null) {
6335     this.container.removeChild(this.canvas);
6336     this.container.removeChild(this.scrollPane);
6337   }
6338   this.preferences.removePropertyChangeListener("UNIT", this.preferencesListener);
6339   this.preferences.removePropertyChangeListener("LANGUAGE", this.preferencesListener);
6340   this.preferences.removePropertyChangeListener("GRID_VISIBLE", this.preferencesListener);
6341   this.preferences.removePropertyChangeListener("DEFAULT_FONT_NAME", this.preferencesListener);
6342   this.preferences.removePropertyChangeListener("FURNITURE_VIEWED_FROM_TOP", this.preferencesListener);
6343   this.preferences.removePropertyChangeListener("FURNITURE_MODEL_ICON_SIZE", this.preferencesListener);
6344   this.preferences.removePropertyChangeListener("ROOM_FLOOR_COLORED_OR_TEXTURED", this.preferencesListener);
6345   this.preferences.removePropertyChangeListener("WALL_PATTERN", this.preferencesListener);
6346 }
6347 
6348 /**
6349  * Returns the component used as an horizontal ruler for this plan.
6350  * @return {Object}
6351  */
6352 PlanComponent.prototype.getHorizontalRuler = function() {
6353   throw new UnsupportedOperationException("No rulers");
6354 }
6355 
6356 /**
6357  * Returns the component used as a vertical ruler for this plan.
6358  * @return {Object}
6359  */
6360 PlanComponent.prototype.getVerticalRuler = function() {
6361   throw new UnsupportedOperationException("No rulers");
6362 }
6363 
6364 
6365 /**
6366  * A proxy for the furniture icon seen from top.
6367  * @param {Image} icon
6368  * @constructor
6369  * @private
6370  */
6371 PlanComponent.PieceOfFurnitureTopViewIcon = function(image) {
6372   this.image = image;
6373 }
6374 
6375 /**
6376  * @ignore
6377  */
6378 PlanComponent.PieceOfFurnitureTopViewIcon.prototype.getIconWidth = function() {
6379   return this.image.naturalWidth;
6380 }
6381 
6382 /**
6383  * @ignore
6384  */
6385 PlanComponent.PieceOfFurnitureTopViewIcon.prototype.getIconHeight = function() {
6386   return this.image.naturalHeight;
6387 }
6388 
6389 /**
6390  * @ignore
6391  */
6392 PlanComponent.PieceOfFurnitureTopViewIcon.prototype.paintIcon = function(g, x, y) {
6393   g.drawImage(this.image, x, y);
6394 }
6395 
6396 /**
6397  * @ignore
6398  */
6399 PlanComponent.PieceOfFurnitureTopViewIcon.prototype.isWaitIcon = function() {
6400   return this.image === TextureManager.getInstance().getWaitImage();
6401 }
6402 
6403 /**
6404  * @ignore
6405  */
6406 PlanComponent.PieceOfFurnitureTopViewIcon.prototype.isErrorIcon = function() {
6407   return this.image === TextureManager.getInstance().getErrorImage();
6408 }
6409 
6410 /**
6411  * @ignore
6412  */
6413 PlanComponent.PieceOfFurnitureTopViewIcon.prototype.setIcon = function(image) {
6414   this.image = image;
6415 }
6416 
6417 
6418 /**
6419  * Creates a plan icon proxy for a <code>piece</code> of furniture.
6420  * @param {HomePieceOfFurniture} piece an object containing a plan icon content
6421  * @param {java.awt.Component} waitingComponent a waiting component. If <code>null</code>, the returned icon will
6422  * be read immediately in the current thread.
6423  * @constructor
6424  * @extends PlanComponent.PieceOfFurnitureTopViewIcon
6425  * @private
6426  */
6427 PlanComponent.PieceOfFurniturePlanIcon = function(piece, waitingComponent) {
6428   PlanComponent.PieceOfFurnitureTopViewIcon.call(this, TextureManager.getInstance().getWaitImage());
6429   var planIcon = this;
6430   TextureManager.getInstance().loadTexture(piece.getPlanIcon(), false, {
6431       textureUpdated: function(textureImage) {
6432         planIcon.setIcon(textureImage);
6433         waitingComponent.repaint();
6434       },
6435       textureError: function(error) {
6436         planIcon.setIcon(TextureManager.getInstance().getErrorImage());
6437         waitingComponent.repaint();
6438       }
6439     });
6440 }
6441 PlanComponent.PieceOfFurniturePlanIcon.prototype = Object.create(PlanComponent.PieceOfFurnitureTopViewIcon.prototype);
6442 PlanComponent.PieceOfFurniturePlanIcon.prototype.constructor = PlanComponent.PieceOfFurniturePlanIcon;
6443 
6444 
6445 /**
6446  * Creates a top view icon proxy for a <code>piece</code> of furniture.
6447  * @param {HomePieceOfFurniture} piece an object containing a 3D content
6448  * @param {Object} object3dFactory a factory with a <code>createObject3D(home, item, waitForLoading)</code> method
6449  * @param {Object} waitingComponent a waiting component. If <code>null</code>, the returned icon will
6450  *          be read immediately in the current thread.
6451  * @param {number} iconSize the size in pixels of the generated icon
6452  * @constructor
6453  * @extends PlanComponent.PieceOfFurnitureTopViewIcon
6454  * @private
6455  */
6456 PlanComponent.PieceOfFurnitureModelIcon = function(piece, object3dFactory, waitingComponent, iconSize) {
6457   PlanComponent.PieceOfFurnitureTopViewIcon.call(this, TextureManager.getInstance().getWaitImage());
6458   var modelIcon = this;
6459   ModelManager.getInstance().loadModel(piece.getModel(), waitingComponent === null, {
6460       modelUpdated: function(modelRoot) {
6461         var normalizedPiece = piece.clone();
6462         if (normalizedPiece.isResizable()
6463             && piece.getRoll() == 0) {
6464           normalizedPiece.setModelMirrored(false);
6465         }
6466         var pieceWidth = normalizedPiece.getWidthInPlan();
6467         var pieceDepth = normalizedPiece.getDepthInPlan();
6468         var pieceHeight = normalizedPiece.getHeightInPlan();
6469         normalizedPiece.setX(0);
6470         normalizedPiece.setY(0);
6471         normalizedPiece.setElevation(-pieceHeight / 2);
6472         normalizedPiece.setLevel(null);
6473         normalizedPiece.setAngle(0);
6474         if (waitingComponent !== null) {
6475           var updater = function() {
6476               object3dFactory.createObject3D(null, normalizedPiece,
6477                   function(pieceNode) {
6478                     modelIcon.createIcon(pieceNode, pieceWidth, pieceDepth, pieceHeight, iconSize, 
6479                         function(icon) {
6480                           modelIcon.setIcon(icon);
6481                           waitingComponent.repaint();
6482                         });
6483                   });
6484             };
6485           setTimeout(updater, 0);
6486         } else {
6487           modelIcon.setIcon(modelIcon.createIcon(object3dFactory.createObject3D(null, normalizedPiece, true), pieceWidth, pieceDepth, pieceHeight, iconSize));
6488         }
6489       },
6490       modelError: function(ex) {
6491         // In case of problem use a default red box
6492         modelIcon.setIcon(TextureManager.getInstance().getErrorImage());
6493         if (waitingComponent !== null) {
6494           waitingComponent.repaint();
6495         }
6496       }
6497     });
6498 }
6499 PlanComponent.PieceOfFurnitureModelIcon.prototype = Object.create(PlanComponent.PieceOfFurnitureTopViewIcon.prototype);
6500 PlanComponent.PieceOfFurnitureModelIcon.prototype.constructor = PlanComponent.PieceOfFurnitureModelIcon;
6501 
6502 /**
6503  * Returns the branch group bound to a universe and a canvas for the given
6504  * resolution.
6505  * @param {number} iconSize
6506  * @return {BranchGroup3D}
6507  * @private
6508  */
6509 PlanComponent.PieceOfFurnitureModelIcon.prototype.getSceneRoot = function(iconSize) {
6510   if (!PlanComponent.PieceOfFurnitureModelIcon.canvas3D
6511       || !PlanComponent.PieceOfFurnitureModelIcon.canvas3D [iconSize]) {
6512     var canvas = document.createElement("canvas");
6513     canvas.width = iconSize;
6514     canvas.height = iconSize;
6515     canvas.style.backgroundColor = "rgba(0, 0, 0, 0)";
6516     var canvas3D = new HTMLCanvas3D(canvas);
6517     var rotation = mat4.create();
6518     mat4.fromXRotation(rotation, -Math.PI / 2);
6519     canvas3D.setViewPlatformTransform(rotation);
6520     canvas3D.setProjectionPolicy(HTMLCanvas3D.PARALLEL_PROJECTION);
6521     canvas3D.setFrontClipDistance(-1.1);
6522     canvas3D.setBackClipDistance(1.1);
6523     var sceneRoot = new BranchGroup3D();
6524     sceneRoot.setCapability(Group3D.ALLOW_CHILDREN_EXTEND);
6525     var lights = [
6526       new DirectionalLight3D(vec3.fromValues(0.6, 0.6, 0.6), vec3.fromValues(1.5, -0.8, -1)),
6527       new DirectionalLight3D(vec3.fromValues(0.6, 0.6, 0.6), vec3.fromValues(-1.5, -0.8, -1)),
6528       new DirectionalLight3D(vec3.fromValues(0.6, 0.6, 0.6), vec3.fromValues(0, -0.8, 1)),
6529       new AmbientLight3D(vec3.fromValues(0.2, 0.2, 0.2))];
6530     for (var i = 0; i < lights.length; i++) {
6531       sceneRoot.addChild(lights[i]);
6532     }
6533     canvas3D.setScene(sceneRoot);
6534     if (!PlanComponent.PieceOfFurnitureModelIcon.canvas3D) {
6535       PlanComponent.PieceOfFurnitureModelIcon.canvas3D = {};
6536     }
6537     PlanComponent.PieceOfFurnitureModelIcon.canvas3D [iconSize] = canvas3D;
6538   }
6539   
6540   if (iconSize !== 128) {
6541     // Keep only canvas for 128 (default) size and the requested icon size
6542     for (var key in PlanComponent.PieceOfFurnitureModelIcon.canvas3D) {
6543       if (key != 128
6544           && key != iconSize
6545           && PlanComponent.PieceOfFurnitureModelIcon.canvas3D.hasOwnProperty(key)) {
6546         PlanComponent.PieceOfFurnitureModelIcon.canvas3D [key].clear();
6547         delete PlanComponent.PieceOfFurnitureModelIcon.canvas3D [key];
6548       }
6549     }
6550   }
6551   return PlanComponent.PieceOfFurnitureModelIcon.canvas3D [iconSize].getScene();
6552 }
6553 
6554 /**
6555  * Creates an icon created and scaled from piece model content, and calls <code>iconObserver</code> once the icon is ready
6556  * or returns the icon itself if <code>iconObserver</code> is not given.
6557  * @param {Object3DBranch} pieceNode
6558  * @param {number} pieceWidth
6559  * @param {number} pieceDepth
6560  * @param {number} pieceHeight
6561  * @param {number} iconSize
6562  * @param {Object} [iconObserver] a function that will receive the icon as parameter
6563  * @return {Image} the icon or <code>undefined</code> if <code>iconObserver</code> exists
6564  * @private
6565  */
6566 PlanComponent.PieceOfFurnitureModelIcon.prototype.createIcon = function(pieceNode, pieceWidth, pieceDepth, pieceHeight, iconSize, iconObserver) {
6567   var scaleTransform = mat4.create();
6568   mat4.scale(scaleTransform, scaleTransform, vec3.fromValues(2 / pieceWidth, 2 / pieceHeight, 2 / pieceDepth));
6569   var modelTransformGroup = new TransformGroup3D();
6570   modelTransformGroup.setTransform(scaleTransform);
6571   if (pieceNode.getParent() != null) {
6572     pieceNode.getParent().removeChild(pieceNode);
6573   }
6574   modelTransformGroup.addChild(pieceNode);
6575   var model = new BranchGroup3D();
6576   model.addChild(modelTransformGroup);
6577   var sceneRoot = this.getSceneRoot(iconSize);
6578   if (iconObserver) {
6579     var observingStart = Date.now();
6580     var iconGeneration = function() {
6581         sceneRoot.addChild(model);
6582         var loadingCompleted = PlanComponent.PieceOfFurnitureModelIcon.canvas3D [iconSize].isLoadingCompleted();
6583         if (loadingCompleted || (Date.now() - observingStart) > 5000) {
6584           PlanComponent.PieceOfFurnitureModelIcon.canvas3D [iconSize].getImage(iconObserver);
6585         }
6586         sceneRoot.removeChild(model);
6587         if (!loadingCompleted) {
6588           setTimeout(iconGeneration, 0);
6589         }
6590       };
6591     iconGeneration();
6592     return undefined;
6593   }
6594   else {
6595     sceneRoot.addChild(model);
6596     var icon = PlanComponent.PieceOfFurnitureModelIcon.canvas3D [iconSize].getImage();
6597     sceneRoot.removeChild(model);
6598     return icon;
6599   }
6600 }
6601 
6602 /**
6603  * Returns the size of the given piece computed from its vertices.
6604  * @param {HomePieceOfFurniture} piece
6605  * @param {Object} object3dFactory
6606  * @return {Array}
6607  * @private
6608  */
6609 PlanComponent.PieceOfFurnitureModelIcon.computePieceOfFurnitureSizeInPlan = function(piece, object3dFactory) {
6610   var horizontalRotation = mat4.create();
6611   if (piece.getPitch() !== 0) {
6612     mat4.fromXRotation(horizontalRotation, -piece.getPitch());
6613   }
6614   if (piece.getRoll() !== 0) {
6615     var rollRotation = mat4.create();
6616     mat4.fromZRotation(rollRotation, -piece.getRoll());
6617     mat4.mul(horizontalRotation, horizontalRotation, rollRotation, horizontalRotation);
6618   }
6619   // Compute bounds of a piece centered at the origin and rotated around the target horizontal angle
6620   piece = piece.clone();
6621   piece.setX(0);
6622   piece.setY(0);
6623   piece.setElevation(-piece.getHeight() / 2);
6624   piece.setLevel(null);
6625   piece.setAngle(0);
6626   piece.setRoll(0);
6627   piece.setPitch(0);
6628   piece.setWidthInPlan(piece.getWidth());
6629   piece.setDepthInPlan(piece.getDepth());
6630   piece.setHeightInPlan(piece.getHeight());
6631   var bounds = ModelManager.getInstance().getBounds(object3dFactory.createObject3D(null, piece, true), horizontalRotation);
6632   var lower = vec3.create();
6633   bounds.getLower(lower);
6634   var upper = vec3.create();
6635   bounds.getUpper(upper);
6636   return [Math.max(0.001, (upper[0] - lower[0])),
6637     Math.max(0.001, (upper[2] - lower[2])),
6638     Math.max(0.001, (upper[1] - lower[1]))];
6639 }
6640 
6641 
6642 /**
6643  * A map key used to compare furniture with the same top view icon.
6644  * @param {HomePieceOfFurniture} piece
6645  * @constructor
6646  * @private
6647  */
6648 PlanComponent.HomePieceOfFurnitureTopViewIconKey = function(piece) {
6649   this.piece = piece;
6650   this.hashCode = (piece.getPlanIcon() != null ? piece.getPlanIcon().hashCode() : piece.getModel().hashCode())
6651       + (piece.getColor() != null ? 37 * piece.getColor() : 1234);
6652   if (piece.isHorizontallyRotated()
6653       || piece.getTexture() != null) {
6654     this.hashCode +=
6655           (piece.getTexture() != null ? 37 * piece.getTexture().hashCode() : 0)
6656         + 37 * (piece.getWidthInPlan() | 0)
6657         + 37 * (piece.getDepthInPlan() | 0)
6658         + 37 * (piece.getHeightInPlan() | 0);
6659   }
6660   if (piece.getRoll() != 0) {
6661     this.hashCode += 37 * (piece.isModelMirrored() ? 1231 : 1237);
6662   }
6663   if (piece.getPlanIcon() != null) {
6664     this.hashCode +=
6665           37 * (function(matrix) { 
6666                    return (31 * matrix[0][0] + 31 * matrix[0][1] + 31 * matrix[0][2]
6667                          + 37 * matrix[1][0] + 37 * matrix[1][1] + 37 * matrix[1][2]
6668                          + 41 * matrix[2][0] + 41 * matrix[2][1] + 41 * matrix[2][2]) | 0; })(piece.getModelRotation())
6669         + 37 * (piece.isModelCenteredAtOrigin() ? 1 : 0)
6670         + 37 * (piece.isBackFaceShown() ? 1 : 0)
6671         + 37 * ((piece.getPitch() * 1000) | 0)
6672         + 37 * ((piece.getRoll() * 1000) | 0)
6673         + 37 * (function(array) {
6674                    var hashCode = 0;
6675                    if (array != null) {
6676                      for (var i = 0; i < array.length; i++) {
6677                        if (array [i] != null) {
6678                          hashCode += 37 * array [i].hashCode(); 
6679                        }
6680                      }
6681                    }
6682                    return hashCode | 0; })(piece.getModelTransformations())
6683         + 37 * (function(array) {
6684                    var hashCode = 0;
6685                    if (array != null) {
6686                      for (var i = 0; i < array.length; i++) {
6687                        if (array [i] != null) {
6688                          hashCode += 37 * array [i].hashCode(); 
6689                        }
6690                      }
6691                    }
6692                    return hashCode | 0; })(piece.getModelMaterials())
6693         + (piece.getShininess() != null ? 37 * ((piece.getShininess() * 1000) | 0) : 3456);
6694   }
6695 }
6696   
6697 /**
6698  * @param {Object} obj
6699  * @return {boolean}
6700  * @private
6701  */
6702 PlanComponent.HomePieceOfFurnitureTopViewIconKey.prototype.equals = function(obj) {
6703   if (obj instanceof PlanComponent.HomePieceOfFurnitureTopViewIconKey) {
6704     var piece2 = obj.piece;
6705     // Test all furniture data that could make change the plan icon
6706     // (see HomePieceOfFurniture3D and PlanComponent#addModelListeners for changes conditions)
6707     return (this.piece.getPlanIcon() != null
6708             ? this.piece.getPlanIcon().equals(piece2.getPlanIcon())
6709             : this.piece.getModel().equals(piece2.getModel()))
6710         && (this.piece.getColor() == piece2.getColor())
6711         && (this.piece.getTexture() == piece2.getTexture()
6712             || this.piece.getTexture() != null && this.piece.getTexture().equals(piece2.getTexture()))
6713         && (!this.piece.isHorizontallyRotated()
6714                 && !piece2.isHorizontallyRotated()
6715                 && this.piece.getTexture() == null
6716                 && piece2.getTexture() == null
6717             || this.piece.getWidthInPlan() == piece2.getWidthInPlan()
6718                 && this.piece.getDepthInPlan() == piece2.getDepthInPlan()
6719                 && this.piece.getHeightInPlan() == piece2.getHeightInPlan())
6720         && (this.piece.getRoll() == 0
6721                 && piece2.getRoll() == 0
6722             || this.piece.isModelMirrored() == piece2.isModelMirrored())
6723         && (this.piece.getPlanIcon() != null
6724             || (function(matrix1, matrix2) { 
6725                   return matrix1[0][0] == matrix2[0][0] && matrix1[0][1] == matrix2[0][1] && matrix1[0][2] == matrix2[0][2]
6726                       && matrix1[1][0] == matrix2[1][0] && matrix1[1][1] == matrix2[1][1] && matrix1[1][2] == matrix2[1][2]
6727                       && matrix1[2][0] == matrix2[2][0] && matrix1[2][1] == matrix2[2][1] && matrix1[2][2] == matrix2[2][2]; })(this.piece.getModelRotation(), piece2.getModelRotation()) 
6728                 && this.piece.isModelCenteredAtOrigin() == piece2.isModelCenteredAtOrigin()
6729                 && this.piece.isBackFaceShown() == piece2.isBackFaceShown()
6730                 && this.piece.getPitch() == piece2.getPitch()
6731                 && this.piece.getRoll() == piece2.getRoll()
6732                 && (function(array1, array2) { 
6733                       if (array1 === array2) return true;
6734                       if (array1 == null || array2 == null || array1.length !== array2.length) return false;
6735                       for (var i = 0; i < array1.length; i++) {
6736                         if (array1[i] !== array2[i] && (array1[i] == null || !array1[i].equals(array2[i]))) return false;
6737                       }
6738                       return true; })(this.piece.getModelTransformations(), piece2.getModelTransformations()) 
6739                 && (function(array1, array2) { 
6740                       if (array1 === array2) return true;
6741                       if (array1 == null || array2 == null || array1.length !== array2.length) return false;
6742                       for (var i = 0; i < array1.length; i++) {
6743                         if (array1[i] !== array2[i] && (array1[i] == null || !array1[i].equals(array2[i]))) return false;
6744                       }
6745                       return true; })(this.piece.getModelMaterials(), piece2.getModelMaterials()) 
6746                 && this.piece.getShininess() == piece2.getShininess());
6747   } else {
6748     return false;
6749   }
6750 }
6751   
6752 /**
6753  * @private
6754  */
6755 PlanComponent.HomePieceOfFurnitureTopViewIconKey.prototype.hashCode = function() {
6756   return this.hashCode;
6757 }
6758