1 /*
2  * Room.java 18 nov. 2008
3  *
4  * Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 package com.eteks.sweethome3d.model;
21 
22 import java.awt.Shape;
23 import java.awt.geom.Area;
24 import java.awt.geom.GeneralPath;
25 import java.awt.geom.PathIterator;
26 import java.awt.geom.Rectangle2D;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.List;
30 
31 /**
32  * A room or a polygon in a home plan.
33  * @author Emmanuel Puybaret
34  */
35 public class Room extends HomeObject implements Selectable, Elevatable {
36   /**
37    * The properties of a room that may change. <code>PropertyChangeListener</code>s added
38    * to a room will be notified under a property name equal to the string value of one these properties.
39    */
40   public enum Property {NAME, NAME_X_OFFSET, NAME_Y_OFFSET, NAME_STYLE, NAME_ANGLE,
41       POINTS, AREA_VISIBLE, AREA_X_OFFSET, AREA_Y_OFFSET, AREA_STYLE, AREA_ANGLE,
42       FLOOR_COLOR, FLOOR_TEXTURE, FLOOR_VISIBLE, FLOOR_SHININESS,
43       CEILING_COLOR, CEILING_TEXTURE, CEILING_VISIBLE, CEILING_SHININESS, LEVEL}
44 
45   private static final long serialVersionUID = 1L;
46 
47   private static final double TWICE_PI = 2 * Math.PI;
48 
49   private String              name;
50   private float               nameXOffset;
51   private float               nameYOffset;
52   private TextStyle           nameStyle;
53   private float               nameAngle;
54   private float [][]          points;
55   private boolean             areaVisible;
56   private float               areaXOffset;
57   private float               areaYOffset;
58   private TextStyle           areaStyle;
59   private float               areaAngle;
60   private boolean             floorVisible;
61   private Integer             floorColor;
62   private HomeTexture         floorTexture;
63   private float               floorShininess;
64   private boolean             ceilingVisible;
65   private Integer             ceilingColor;
66   private HomeTexture         ceilingTexture;
67   private float               ceilingShininess;
68   private Level               level;
69 
70   private transient Shape     shapeCache;
71   private transient Float     areaCache;
72 
73   /**
74    * Creates a room from its name and the given coordinates.
75    */
Room(float [][] points)76   public Room(float [][] points) {
77     this(createId("room"), points);
78   }
79 
80   /**
81    * Creates a room from its name and the given coordinates.
82    */
Room(String id, float [][] points)83   public Room(String id, float [][] points) {
84     super(id);
85     if (points.length <= 1) {
86       throw new IllegalStateException("Room points must containt at least two points");
87     }
88     this.points = deepCopy(points);
89     this.areaVisible = true;
90     this.nameYOffset = -40f;
91     this.floorVisible = true;
92     this.ceilingVisible = true;
93   }
94 
95   /**
96    * Returns the name of this room.
97    */
getName()98   public String getName() {
99     return this.name;
100   }
101 
102   /**
103    * Sets the name of this room. Once this room is updated,
104    * listeners added to this room will receive a change notification.
105    */
setName(String name)106   public void setName(String name) {
107     if (name != this.name
108         && (name == null || !name.equals(this.name))) {
109       String oldName = this.name;
110       this.name = name;
111       firePropertyChange(Property.NAME.name(), oldName, name);
112     }
113   }
114 
115   /**
116    * Returns the distance along x axis applied to room center abscissa
117    * to display room name.
118    */
getNameXOffset()119   public float getNameXOffset() {
120     return this.nameXOffset;
121   }
122 
123   /**
124    * Sets the distance along x axis applied to room center abscissa to display room name.
125    * Once this room  is updated, listeners added to this room will receive a change notification.
126    */
setNameXOffset(float nameXOffset)127   public void setNameXOffset(float nameXOffset) {
128     if (nameXOffset != this.nameXOffset) {
129       float oldNameXOffset = this.nameXOffset;
130       this.nameXOffset = nameXOffset;
131       firePropertyChange(Property.NAME_X_OFFSET.name(), oldNameXOffset, nameXOffset);
132     }
133   }
134 
135   /**
136    * Returns the distance along y axis applied to room center ordinate
137    * to display room name.
138    */
getNameYOffset()139   public float getNameYOffset() {
140     return this.nameYOffset;
141   }
142 
143   /**
144    * Sets the distance along y axis applied to room center ordinate to display room name.
145    * Once this room is updated, listeners added to this room will receive a change notification.
146    */
setNameYOffset(float nameYOffset)147   public void setNameYOffset(float nameYOffset) {
148     if (nameYOffset != this.nameYOffset) {
149       float oldNameYOffset = this.nameYOffset;
150       this.nameYOffset = nameYOffset;
151       firePropertyChange(Property.NAME_Y_OFFSET.name(), oldNameYOffset, nameYOffset);
152     }
153   }
154 
155   /**
156    * Returns the text style used to display room name.
157    */
getNameStyle()158   public TextStyle getNameStyle() {
159     return this.nameStyle;
160   }
161 
162   /**
163    * Sets the text style used to display room name.
164    * Once this room is updated, listeners added to this room will receive a change notification.
165    */
setNameStyle(TextStyle nameStyle)166   public void setNameStyle(TextStyle nameStyle) {
167     if (nameStyle != this.nameStyle) {
168       TextStyle oldNameStyle = this.nameStyle;
169       this.nameStyle = nameStyle;
170       firePropertyChange(Property.NAME_STYLE.name(), oldNameStyle, nameStyle);
171     }
172   }
173 
174   /**
175    * Returns the angle in radians used to display the room name.
176    * @since 3.6
177    */
getNameAngle()178   public float getNameAngle() {
179     return this.nameAngle;
180   }
181 
182   /**
183    * Sets the angle in radians used to display the room name. Once this piece is updated,
184    * listeners added to this piece will receive a change notification.
185    * @since 3.6
186    */
setNameAngle(float nameAngle)187   public void setNameAngle(float nameAngle) {
188     // Ensure angle is always positive and between 0 and 2 PI
189     nameAngle = (float)((nameAngle % TWICE_PI + TWICE_PI) % TWICE_PI);
190     if (nameAngle != this.nameAngle) {
191       float oldNameAngle = this.nameAngle;
192       this.nameAngle = nameAngle;
193       firePropertyChange(Property.NAME_ANGLE.name(), oldNameAngle, nameAngle);
194     }
195   }
196 
197   /**
198    * Returns the points of the polygon matching this room.
199    * @return an array of the (x,y) coordinates of the room points.
200    */
getPoints()201   public float [][] getPoints() {
202     return deepCopy(this.points);
203   }
204 
205   /**
206    * Returns the number of points of the polygon matching this room.
207    * @since 2.0
208    */
getPointCount()209   public int getPointCount() {
210     return this.points.length;
211   }
212 
deepCopy(float [][] points)213   private float [][] deepCopy(float [][] points) {
214     float [][] pointsCopy = new float [points.length][];
215     for (int i = 0; i < points.length; i++) {
216       pointsCopy [i] = points [i].clone();
217     }
218     return pointsCopy;
219   }
220 
221   /**
222    * Sets the points of the polygon matching this room. Once this room
223    * is updated, listeners added to this room will receive a change notification.
224    */
setPoints(float [][] points)225   public void setPoints(float [][] points) {
226     if (!Arrays.deepEquals(this.points, points)) {
227       updatePoints(points);
228     }
229   }
230 
231   /**
232    * Update the points of the polygon matching this room.
233    */
updatePoints(float [][] points)234   private void updatePoints(float [][] points) {
235     float [][] oldPoints = this.points;
236     this.points = deepCopy(points);
237     this.shapeCache = null;
238     this.areaCache  = null;
239     firePropertyChange(Property.POINTS.name(), oldPoints, points);
240   }
241 
242   /**
243    * Adds a point at the end of room points.
244    * @since 2.0
245    */
addPoint(float x, float y)246   public void addPoint(float x, float y) {
247     addPoint(x, y, this.points.length);
248   }
249 
250   /**
251    * Adds a point at the given <code>index</code>.
252    * @throws IndexOutOfBoundsException if <code>index</code> is negative or > <code>getPointCount()</code>
253    * @since 2.0
254    */
addPoint(float x, float y, int index)255   public void addPoint(float x, float y, int index) {
256     if (index < 0 || index > this.points.length) {
257       throw new IndexOutOfBoundsException("Invalid index " + index);
258     }
259 
260     float [][] newPoints = new float [this.points.length + 1][];
261     System.arraycopy(this.points, 0, newPoints, 0, index);
262     newPoints [index] = new float [] {x, y};
263     System.arraycopy(this.points, index, newPoints, index + 1, this.points.length - index);
264 
265     float [][] oldPoints = this.points;
266     this.points = newPoints;
267     this.shapeCache = null;
268     this.areaCache  = null;
269     firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
270   }
271 
272   /**
273    * Sets the point at the given <code>index</code>.
274    * @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
275    * @since 2.0
276    */
setPoint(float x, float y, int index)277   public void setPoint(float x, float y, int index) {
278     if (index < 0 || index >= this.points.length) {
279       throw new IndexOutOfBoundsException("Invalid index " + index);
280     }
281     if (this.points [index][0] != x
282         || this.points [index][1] != y) {
283       float [][] oldPoints = this.points;
284       this.points = deepCopy(this.points);
285       this.points [index][0] = x;
286       this.points [index][1] = y;
287       this.shapeCache = null;
288       this.areaCache  = null;
289       firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
290     }
291   }
292 
293   /**
294    * Removes the point at the given <code>index</code>.
295    * @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
296    * @since 2.0
297    */
removePoint(int index)298   public void removePoint(int index) {
299     if (index < 0 || index >= this.points.length) {
300       throw new IndexOutOfBoundsException("Invalid index " + index);
301     } else if (this.points.length <= 1) {
302       throw new IllegalStateException("Room points must containt at least one point");
303     }
304 
305     float [][] newPoints = new float [this.points.length - 1][];
306     System.arraycopy(this.points, 0, newPoints, 0, index);
307     System.arraycopy(this.points, index + 1, newPoints, index, this.points.length - index - 1);
308 
309     float [][] oldPoints = this.points;
310     this.points = newPoints;
311     this.shapeCache = null;
312     this.areaCache  = null;
313     firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
314   }
315 
316   /**
317    * Returns whether the area of this room is visible or not.
318    */
isAreaVisible()319   public boolean isAreaVisible() {
320     return this.areaVisible;
321   }
322 
323   /**
324    * Sets whether the area of this room is visible or not. Once this room
325    * is updated, listeners added to this room will receive a change notification.
326    */
setAreaVisible(boolean areaVisible)327   public void setAreaVisible(boolean areaVisible) {
328     if (areaVisible != this.areaVisible) {
329       this.areaVisible = areaVisible;
330       firePropertyChange(Property.AREA_VISIBLE.name(), !areaVisible, areaVisible);
331     }
332   }
333 
334   /**
335    * Returns the distance along x axis applied to room center abscissa
336    * to display room area.
337    */
getAreaXOffset()338   public float getAreaXOffset() {
339     return this.areaXOffset;
340   }
341 
342   /**
343    * Sets the distance along x axis applied to room center abscissa to display room area.
344    * Once this room  is updated, listeners added to this room will receive a change notification.
345    */
setAreaXOffset(float areaXOffset)346   public void setAreaXOffset(float areaXOffset) {
347     if (areaXOffset != this.areaXOffset) {
348       float oldAreaXOffset = this.areaXOffset;
349       this.areaXOffset = areaXOffset;
350       firePropertyChange(Property.AREA_X_OFFSET.name(), oldAreaXOffset, areaXOffset);
351     }
352   }
353 
354   /**
355    * Returns the distance along y axis applied to room center ordinate
356    * to display room area.
357    */
getAreaYOffset()358   public float getAreaYOffset() {
359     return this.areaYOffset;
360   }
361 
362   /**
363    * Sets the distance along y axis applied to room center ordinate to display room area.
364    * Once this room is updated, listeners added to this room will receive a change notification.
365    */
setAreaYOffset(float areaYOffset)366   public void setAreaYOffset(float areaYOffset) {
367     if (areaYOffset != this.areaYOffset) {
368       float oldAreaYOffset = this.areaYOffset;
369       this.areaYOffset = areaYOffset;
370       firePropertyChange(Property.AREA_Y_OFFSET.name(), oldAreaYOffset, areaYOffset);
371     }
372   }
373 
374   /**
375    * Returns the text style used to display room area.
376    */
getAreaStyle()377   public TextStyle getAreaStyle() {
378     return this.areaStyle;
379   }
380 
381   /**
382    * Sets the text style used to display room area.
383    * Once this room is updated, listeners added to this room will receive a change notification.
384    */
setAreaStyle(TextStyle areaStyle)385   public void setAreaStyle(TextStyle areaStyle) {
386     if (areaStyle != this.areaStyle) {
387       TextStyle oldAreaStyle = this.areaStyle;
388       this.areaStyle = areaStyle;
389       firePropertyChange(Property.AREA_STYLE.name(), oldAreaStyle, areaStyle);
390     }
391   }
392 
393   /**
394    * Returns the angle in radians used to display the room area.
395    * @since 3.6
396    */
getAreaAngle()397   public float getAreaAngle() {
398     return this.areaAngle;
399   }
400 
401   /**
402    * Sets the angle in radians used to display the room area. Once this piece is updated,
403    * listeners added to this piece will receive a change notification.
404    * @since 3.6
405    */
setAreaAngle(float areaAngle)406   public void setAreaAngle(float areaAngle) {
407     // Ensure angle is always positive and between 0 and 2 PI
408     areaAngle = (float)((areaAngle % TWICE_PI + TWICE_PI) % TWICE_PI);
409     if (areaAngle != this.areaAngle) {
410       float oldAreaAngle = this.areaAngle;
411       this.areaAngle = areaAngle;
412       firePropertyChange(Property.AREA_ANGLE.name(), oldAreaAngle, areaAngle);
413     }
414   }
415 
416   /**
417    * Returns the abscissa of the center point of this room.
418    */
getXCenter()419   public float getXCenter() {
420     float xMin = this.points [0][0];
421     float xMax = this.points [0][0];
422     for (int i = 1; i < this.points.length; i++) {
423       xMin = Math.min(xMin, this.points [i][0]);
424       xMax = Math.max(xMax, this.points [i][0]);
425     }
426     return (xMin + xMax) / 2;
427   }
428 
429   /**
430    * Returns the ordinate of the center point of this room.
431    */
getYCenter()432   public float getYCenter() {
433     float yMin = this.points [0][1];
434     float yMax = this.points [0][1];
435     for (int i = 1; i < this.points.length; i++) {
436       yMin = Math.min(yMin, this.points [i][1]);
437       yMax = Math.max(yMax, this.points [i][1]);
438     }
439     return (yMin + yMax) / 2;
440   }
441 
442   /**
443    * Returns the floor color of this room.
444    */
getFloorColor()445   public Integer getFloorColor() {
446     return this.floorColor;
447   }
448 
449   /**
450    * Sets the floor color of this room. Once this room is updated,
451    * listeners added to this room will receive a change notification.
452    */
setFloorColor(Integer floorColor)453   public void setFloorColor(Integer floorColor) {
454     if (floorColor != this.floorColor
455         && (floorColor == null || !floorColor.equals(this.floorColor))) {
456       Integer oldFloorColor = this.floorColor;
457       this.floorColor = floorColor;
458       firePropertyChange(Property.FLOOR_COLOR.name(), oldFloorColor, floorColor);
459     }
460   }
461 
462   /**
463    * Returns the floor texture of this room.
464    */
getFloorTexture()465   public HomeTexture getFloorTexture() {
466     return this.floorTexture;
467   }
468 
469   /**
470    * Sets the floor texture of this room. Once this room is updated,
471    * listeners added to this room will receive a change notification.
472    */
setFloorTexture(HomeTexture floorTexture)473   public void setFloorTexture(HomeTexture floorTexture) {
474     if (floorTexture != this.floorTexture
475         && (floorTexture == null || !floorTexture.equals(this.floorTexture))) {
476       HomeTexture oldFloorTexture = this.floorTexture;
477       this.floorTexture = floorTexture;
478       firePropertyChange(Property.FLOOR_TEXTURE.name(), oldFloorTexture, floorTexture);
479     }
480   }
481 
482   /**
483    * Returns whether the floor of this room is visible or not.
484    */
isFloorVisible()485   public boolean isFloorVisible() {
486     return this.floorVisible;
487   }
488 
489   /**
490    * Sets whether the floor of this room is visible or not. Once this room
491    * is updated, listeners added to this room will receive a change notification.
492    */
setFloorVisible(boolean floorVisible)493   public void setFloorVisible(boolean floorVisible) {
494     if (floorVisible != this.floorVisible) {
495       this.floorVisible = floorVisible;
496       firePropertyChange(Property.FLOOR_VISIBLE.name(), !floorVisible, floorVisible);
497     }
498   }
499 
500   /**
501    * Returns the floor shininess of this room.
502    * @return a value between 0 (matt) and 1 (very shiny)
503    * @since 3.0
504    */
getFloorShininess()505   public float getFloorShininess() {
506     return this.floorShininess;
507   }
508 
509   /**
510    * Sets the floor shininess of this room. Once this room is updated,
511    * listeners added to this room will receive a change notification.
512    * @since 3.0
513    */
setFloorShininess(float floorShininess)514   public void setFloorShininess(float floorShininess) {
515     if (floorShininess != this.floorShininess) {
516       float oldFloorShininess = this.floorShininess;
517       this.floorShininess = floorShininess;
518       firePropertyChange(Property.FLOOR_SHININESS.name(), oldFloorShininess, floorShininess);
519     }
520   }
521 
522   /**
523    * Returns the ceiling color color of this room.
524    */
getCeilingColor()525   public Integer getCeilingColor() {
526     return this.ceilingColor;
527   }
528 
529   /**
530    * Sets the ceiling color of this room. Once this room is updated,
531    * listeners added to this room will receive a change notification.
532    */
setCeilingColor(Integer ceilingColor)533   public void setCeilingColor(Integer ceilingColor) {
534     if (ceilingColor != this.ceilingColor
535         && (ceilingColor == null || !ceilingColor.equals(this.ceilingColor))) {
536       Integer oldCeilingColor = this.ceilingColor;
537       this.ceilingColor = ceilingColor;
538       firePropertyChange(Property.CEILING_COLOR.name(), oldCeilingColor, ceilingColor);
539     }
540   }
541 
542   /**
543    * Returns the ceiling texture of this room.
544    */
getCeilingTexture()545   public HomeTexture getCeilingTexture() {
546     return this.ceilingTexture;
547   }
548 
549   /**
550    * Sets the ceiling texture of this room. Once this room is updated,
551    * listeners added to this room will receive a change notification.
552    */
setCeilingTexture(HomeTexture ceilingTexture)553   public void setCeilingTexture(HomeTexture ceilingTexture) {
554     if (ceilingTexture != this.ceilingTexture
555         && (ceilingTexture == null || !ceilingTexture.equals(this.ceilingTexture))) {
556       HomeTexture oldCeilingTexture = this.ceilingTexture;
557       this.ceilingTexture = ceilingTexture;
558       firePropertyChange(Property.CEILING_TEXTURE.name(), oldCeilingTexture, ceilingTexture);
559     }
560   }
561 
562   /**
563    * Returns whether the ceiling of this room is visible or not.
564    */
isCeilingVisible()565   public boolean isCeilingVisible() {
566     return this.ceilingVisible;
567   }
568 
569   /**
570    * Sets whether the ceiling of this room is visible or not. Once this room
571    * is updated, listeners added to this room will receive a change notification.
572    */
setCeilingVisible(boolean ceilingVisible)573   public void setCeilingVisible(boolean ceilingVisible) {
574     if (ceilingVisible != this.ceilingVisible) {
575       this.ceilingVisible = ceilingVisible;
576       firePropertyChange(Property.CEILING_VISIBLE.name(), !ceilingVisible, ceilingVisible);
577     }
578   }
579 
580   /**
581    * Returns the ceiling shininess of this room.
582    * @return a value between 0 (matt) and 1 (very shiny)
583    * @since 3.0
584    */
getCeilingShininess()585   public float getCeilingShininess() {
586     return this.ceilingShininess;
587   }
588 
589   /**
590    * Sets the ceiling shininess of this room. Once this room is updated,
591    * listeners added to this room will receive a change notification.
592    * @since 3.0
593    */
setCeilingShininess(float ceilingShininess)594   public void setCeilingShininess(float ceilingShininess) {
595     if (ceilingShininess != this.ceilingShininess) {
596       float oldCeilingShininess = this.ceilingShininess;
597       this.ceilingShininess = ceilingShininess;
598       firePropertyChange(Property.CEILING_SHININESS.name(), oldCeilingShininess, ceilingShininess);
599     }
600   }
601 
602   /**
603    * Returns the level which this room belongs to.
604    * @since 3.4
605    */
getLevel()606   public Level getLevel() {
607     return this.level;
608   }
609 
610   /**
611    * Sets the level of this room. Once this room is updated,
612    * listeners added to this room will receive a change notification.
613    * @since 3.4
614    */
setLevel(Level level)615   public void setLevel(Level level) {
616     if (level != this.level) {
617       Level oldLevel = this.level;
618       this.level = level;
619       firePropertyChange(Property.LEVEL.name(), oldLevel, level);
620     }
621   }
622 
623   /**
624    * Returns <code>true</code> if this room is at the given <code>level</code>
625    * or at a level with the same elevation and a smaller elevation index.
626    * @since 3.4
627    */
isAtLevel(Level level)628   public boolean isAtLevel(Level level) {
629     return this.level == level
630         || this.level != null && level != null
631            && this.level.getElevation() == level.getElevation()
632            && this.level.getElevationIndex() < level.getElevationIndex();
633   }
634 
635   /**
636    * Returns the area of this room.
637    */
getArea()638   public float getArea() {
639     if (this.areaCache == null) {
640       Area roomArea = new Area(getShape());
641       if (roomArea.isSingular()) {
642         this.areaCache = Math.abs(getSignedArea(getPoints()));
643       } else {
644         // Add the surface of the different polygons of this room
645         float area = 0;
646         List<float []> currentPathPoints = new ArrayList<float[]>();
647         for (PathIterator it = roomArea.getPathIterator(null); !it.isDone(); ) {
648           float [] roomPoint = new float[2];
649           switch (it.currentSegment(roomPoint)) {
650             case PathIterator.SEG_MOVETO :
651               currentPathPoints.add(roomPoint);
652               break;
653             case PathIterator.SEG_LINETO :
654               currentPathPoints.add(roomPoint);
655               break;
656             case PathIterator.SEG_CLOSE :
657               float [][] pathPoints =
658                   currentPathPoints.toArray(new float [currentPathPoints.size()][]);
659               area += getSignedArea(pathPoints);
660               currentPathPoints.clear();
661               break;
662           }
663           it.next();
664         }
665         this.areaCache = area;
666       }
667     }
668     return this.areaCache;
669   }
670 
getSignedArea(float areaPoints [][])671   private float getSignedArea(float areaPoints [][]) {
672     // From "Area of a General Polygon" algorithm described in
673     // http://www.davidchandler.com/AreaOfAGeneralPolygon.pdf
674     double area = 0; // Compute in double to avoid precision loss with complex areas
675     for (int i = 1; i < areaPoints.length; i++) {
676       area += (double)areaPoints [i][0] * areaPoints [i - 1][1];
677       area -= (double)areaPoints [i][1] * areaPoints [i - 1][0];
678     }
679     area += (double)areaPoints [0][0] * areaPoints [areaPoints.length - 1][1];
680     area -= (double)areaPoints [0][1] * areaPoints [areaPoints.length - 1][0];
681     return (float)area / 2;
682   }
683 
684   /**
685    * Returns <code>true</code> if the points of this room are in clockwise order.
686    */
isClockwise()687   public boolean isClockwise() {
688     return getSignedArea(getPoints()) < 0;
689   }
690 
691   /**
692    * Returns <code>true</code> if this room is comprised of only one polygon.
693    */
isSingular()694   public boolean isSingular() {
695     return new Area(getShape()).isSingular();
696   }
697 
698   /**
699    * Returns <code>true</code> if this room intersects
700    * with the horizontal rectangle which opposite corners are at points
701    * (<code>x0</code>, <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
702    */
intersectsRectangle(float x0, float y0, float x1, float y1)703   public boolean intersectsRectangle(float x0, float y0, float x1, float y1) {
704     Rectangle2D rectangle = new Rectangle2D.Float(x0, y0, 0, 0);
705     rectangle.add(x1, y1);
706     return getShape().intersects(rectangle);
707   }
708 
709   /**
710    * Returns <code>true</code> if this room contains
711    * the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
712    */
containsPoint(float x, float y, float margin)713   public boolean containsPoint(float x, float y, float margin) {
714     return containsShapeAtWithMargin(getShape(), x, y, margin);
715   }
716 
717   /**
718    * Returns the index of the point of this room equal to
719    * the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
720    * @return the index of the first found point or -1.
721    */
getPointIndexAt(float x, float y, float margin)722   public int getPointIndexAt(float x, float y, float margin) {
723     for (int i = 0; i < this.points.length; i++) {
724       if (Math.abs(x - this.points [i][0]) <= margin && Math.abs(y - this.points [i][1]) <= margin) {
725         return i;
726       }
727     }
728     return -1;
729   }
730 
731   /**
732    * Returns <code>true</code> if the center point at which is displayed the name
733    * of this room is equal to the point at (<code>x</code>, <code>y</code>)
734    * with a given <code>margin</code>.
735    */
isNameCenterPointAt(float x, float y, float margin)736   public boolean isNameCenterPointAt(float x, float y, float margin) {
737     return Math.abs(x - getXCenter() - getNameXOffset()) <= margin
738         && Math.abs(y - getYCenter() - getNameYOffset()) <= margin;
739   }
740 
741   /**
742    * Returns <code>true</code> if the center point at which is displayed the area
743    * of this room is equal to the point at (<code>x</code>, <code>y</code>)
744    * with a given <code>margin</code>.
745    */
isAreaCenterPointAt(float x, float y, float margin)746   public boolean isAreaCenterPointAt(float x, float y, float margin) {
747     return Math.abs(x - getXCenter() - getAreaXOffset()) <= margin
748         && Math.abs(y - getYCenter() - getAreaYOffset()) <= margin;
749   }
750 
751   /**
752    * Returns <code>true</code> if <code>shape</code> contains
753    * the point at (<code>x</code>, <code>y</code>)
754    * with a given <code>margin</code>.
755    */
containsShapeAtWithMargin(Shape shape, float x, float y, float margin)756   private boolean containsShapeAtWithMargin(Shape shape, float x, float y, float margin) {
757     if (margin == 0) {
758       return shape.contains(x, y);
759     } else {
760       return shape.intersects(x - margin, y - margin, 2 * margin, 2 * margin);
761     }
762   }
763 
764   /**
765    * Returns the shape matching this room.
766    */
getShape()767   private Shape getShape() {
768     if (this.shapeCache == null) {
769       GeneralPath roomShape = new GeneralPath();
770       roomShape.moveTo(this.points [0][0], this.points [0][1]);
771       for (int i = 1; i < this.points.length; i++) {
772         roomShape.lineTo(this.points [i][0], this.points [i][1]);
773       }
774       roomShape.closePath();
775       // Cache roomShape
776       this.shapeCache = roomShape;
777     }
778     return this.shapeCache;
779   }
780 
781   /**
782    * Moves this room of (<code>dx</code>, <code>dy</code>) units.
783    */
move(float dx, float dy)784   public void move(float dx, float dy) {
785     if (dx != 0 || dy != 0) {
786       float [][] points = getPoints();
787       for (int i = 0; i < points.length; i++) {
788         points [i][0] += dx;
789         points [i][1] += dy;
790       }
791       updatePoints(points);
792     }
793   }
794 
795   /**
796    * Returns a clone of this room.
797    */
798   @Override
clone()799   public Room clone() {
800     Room clone = (Room)super.clone();
801     clone.level = null;
802     return clone;
803   }
804 }
805