1 /*
2  * DimensionLine.java 17 sept 2007
3  *
4  * Sweet Home 3D, Copyright (c) 2007 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.GeneralPath;
24 import java.awt.geom.Line2D;
25 import java.awt.geom.Point2D;
26 import java.awt.geom.Rectangle2D;
27 
28 /**
29  * A dimension line in plan.
30  * @author Emmanuel Puybaret
31  */
32 public class DimensionLine extends HomeObject implements Selectable, Elevatable {
33   /**
34    * The properties of a dimension line that may change. <code>PropertyChangeListener</code>s added
35    * to a dimension line will be notified under a property name equal to the string value of one these properties.
36    */
37   public enum Property {X_START, Y_START, X_END, Y_END, OFFSET, LENGTH_STYLE, LEVEL}
38 
39   private static final long serialVersionUID = 1L;
40 
41   private float               xStart;
42   private float               yStart;
43   private float               xEnd;
44   private float               yEnd;
45   private float               offset;
46   private TextStyle           lengthStyle;
47   private Level               level;
48 
49   private transient Shape shapeCache;
50 
51   /**
52    * Creates a dimension line from (<code>xStart</code>,<code>yStart</code>)
53    * to (<code>xEnd</code>, <code>yEnd</code>), with a given offset.
54    */
DimensionLine(float xStart, float yStart, float xEnd, float yEnd, float offset)55   public DimensionLine(float xStart, float yStart, float xEnd, float yEnd, float offset) {
56     this(createId("dimensionLine"), xStart, yStart, xEnd, yEnd, offset);
57   }
58 
59   /**
60    * Creates a dimension line from (<code>xStart</code>,<code>yStart</code>)
61    * to (<code>xEnd</code>, <code>yEnd</code>), with a given offset.
62    * @since 6.4
63    */
DimensionLine(String id, float xStart, float yStart, float xEnd, float yEnd, float offset)64   public DimensionLine(String id, float xStart, float yStart, float xEnd, float yEnd, float offset) {
65     super(id);
66     this.xStart = xStart;
67     this.yStart = yStart;
68     this.xEnd = xEnd;
69     this.yEnd = yEnd;
70     this.offset = offset;
71   }
72 
73   /**
74    * Returns the start point abscissa of this dimension line.
75    */
getXStart()76   public float getXStart() {
77     return this.xStart;
78   }
79 
80   /**
81    * Sets the start point abscissa of this dimension line. Once this dimension line
82    * is updated, listeners added to this dimension line will receive a change notification.
83    */
setXStart(float xStart)84   public void setXStart(float xStart) {
85     if (xStart != this.xStart) {
86       float oldXStart = this.xStart;
87       this.xStart = xStart;
88       this.shapeCache = null;
89       firePropertyChange(Property.X_START.name(), oldXStart, xStart);
90     }
91   }
92 
93   /**
94    * Returns the start point ordinate of this dimension line.
95    */
getYStart()96   public float getYStart() {
97     return this.yStart;
98   }
99 
100   /**
101    * Sets the start point ordinate of this dimension line. Once this dimension line
102    * is updated, listeners added to this dimension line will receive a change notification.
103    */
setYStart(float yStart)104   public void setYStart(float yStart) {
105     if (yStart != this.yStart) {
106       float oldYStart = this.yStart;
107       this.yStart = yStart;
108       this.shapeCache = null;
109       firePropertyChange(Property.Y_START.name(), oldYStart, yStart);
110     }
111   }
112 
113   /**
114    * Returns the end point abscissa of this dimension line.
115    */
getXEnd()116   public float getXEnd() {
117     return this.xEnd;
118   }
119 
120   /**
121    * Sets the end point abscissa of this dimension line. Once this dimension line
122    * is updated, listeners added to this dimension line will receive a change notification.
123    */
setXEnd(float xEnd)124   public void setXEnd(float xEnd) {
125     if (xEnd != this.xEnd) {
126       float oldXEnd = this.xEnd;
127       this.xEnd = xEnd;
128       this.shapeCache = null;
129       firePropertyChange(Property.X_END.name(), oldXEnd, xEnd);
130     }
131   }
132 
133   /**
134    * Returns the end point ordinate of this dimension line.
135    */
getYEnd()136   public float getYEnd() {
137     return this.yEnd;
138   }
139 
140   /**
141    * Sets the end point ordinate of this dimension line. Once this dimension line
142    * is updated, listeners added to this dimension line will receive a change notification.
143    */
setYEnd(float yEnd)144   public void setYEnd(float yEnd) {
145     if (yEnd != this.yEnd) {
146       float oldYEnd = this.yEnd;
147       this.yEnd = yEnd;
148       this.shapeCache = null;
149       firePropertyChange(Property.Y_END.name(), oldYEnd, yEnd);
150     }
151   }
152 
153   /**
154    * Returns the offset of this dimension line.
155    */
getOffset()156   public float getOffset() {
157     return this.offset;
158   }
159 
160   /**
161    * Sets the offset of this dimension line.  Once this dimension line
162    * is updated, listeners added to this dimension line will receive a change notification.
163    */
setOffset(float offset)164   public void setOffset(float offset) {
165     if (offset != this.offset) {
166       float oldOffset = this.offset;
167       this.offset = offset;
168       this.shapeCache = null;
169       firePropertyChange(Property.OFFSET.name(), oldOffset, offset);
170     }
171   }
172 
173   /**
174    * Returns the length of this dimension line.
175    */
getLength()176   public float getLength() {
177     return (float)Point2D.distance(getXStart(), getYStart(), getXEnd(), getYEnd());
178   }
179 
180   /**
181    * Returns the text style used to display dimension line length.
182    */
getLengthStyle()183   public TextStyle getLengthStyle() {
184     return this.lengthStyle;
185   }
186 
187   /**
188    * Sets the text style used to display dimension line length.
189    * Once this dimension line is updated, listeners added to it will receive a change notification.
190    */
setLengthStyle(TextStyle lengthStyle)191   public void setLengthStyle(TextStyle lengthStyle) {
192     if (lengthStyle != this.lengthStyle) {
193       TextStyle oldLengthStyle = this.lengthStyle;
194       this.lengthStyle = lengthStyle;
195       firePropertyChange(Property.LENGTH_STYLE.name(), oldLengthStyle, lengthStyle);
196     }
197   }
198 
199   /**
200    * Returns the level which this dimension line belongs to.
201    * @since 3.4
202    */
getLevel()203   public Level getLevel() {
204     return this.level;
205   }
206 
207   /**
208    * Sets the level of this dimension line. Once this dimension line is updated,
209    * listeners added to this dimension line will receive a change notification.
210    * @since 3.4
211    */
setLevel(Level level)212   public void setLevel(Level level) {
213     if (level != this.level) {
214       Level oldLevel = this.level;
215       this.level = level;
216       firePropertyChange(Property.LEVEL.name(), oldLevel, level);
217     }
218   }
219 
220   /**
221    * Returns <code>true</code> if this dimension line is at the given <code>level</code>
222    * or at a level with the same elevation and a smaller elevation index.
223    * @since 3.4
224    */
isAtLevel(Level level)225   public boolean isAtLevel(Level level) {
226     return this.level == level
227         || this.level != null && level != null
228            && this.level.getElevation() == level.getElevation()
229            && this.level.getElevationIndex() < level.getElevationIndex();
230 
231   }
232 
233   /**
234    * Returns the points of the rectangle surrounding
235    * this dimension line and its extension lines.
236    * @return an array of the 4 (x,y) coordinates of the rectangle.
237    */
getPoints()238   public float [][] getPoints() {
239     double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
240     float dx = (float)-Math.sin(angle) * this.offset;
241     float dy = (float)Math.cos(angle) * this.offset;
242 
243     return new float [] [] {{this.xStart, this.yStart},
244                             {this.xStart + dx, this.yStart + dy},
245                             {this.xEnd + dx, this.yEnd + dy},
246                             {this.xEnd, this.yEnd}};
247   }
248 
249   /**
250    * Returns <code>true</code> if this dimension line intersects
251    * with the horizontal rectangle which opposite corners are at points
252    * (<code>x0</code>, <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
253    */
intersectsRectangle(float x0, float y0, float x1, float y1)254   public boolean intersectsRectangle(float x0, float y0, float x1, float y1) {
255     Rectangle2D rectangle = new Rectangle2D.Float(x0, y0, 0, 0);
256     rectangle.add(x1, y1);
257     return getShape().intersects(rectangle);
258   }
259 
260   /**
261    * Returns <code>true</code> if this dimension line contains
262    * the point at (<code>x</code>, <code>y</code>)
263    * with a given <code>margin</code>.
264    */
containsPoint(float x, float y, float margin)265   public boolean containsPoint(float x, float y, float margin) {
266     return containsShapeAtWithMargin(getShape(), x, y, margin);
267   }
268 
269   /**
270    * Returns <code>true</code> if the middle point of this dimension line
271    * is the point at (<code>x</code>, <code>y</code>)
272    * with a given <code>margin</code>.
273    */
isMiddlePointAt(float x, float y, float margin)274   public boolean isMiddlePointAt(float x, float y, float margin) {
275     double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
276     float dx = (float)-Math.sin(angle) * this.offset;
277     float dy = (float)Math.cos(angle) * this.offset;
278     float xMiddle = (this.xStart + this.xEnd) / 2 + dx;
279     float yMiddle = (this.yStart + this.yEnd) / 2 + dy;
280     return Math.abs(x - xMiddle) <= margin && Math.abs(y - yMiddle) <= margin;
281   }
282 
283   /**
284    * Returns <code>true</code> if the extension line at the start of this dimension line
285    * contains the point at (<code>x</code>, <code>y</code>)
286    * with a given <code>margin</code> around the extension line.
287    */
containsStartExtensionLinetAt(float x, float y, float margin)288   public boolean containsStartExtensionLinetAt(float x, float y, float margin) {
289     double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
290     Line2D startExtensionLine = new Line2D.Float(this.xStart, this.yStart,
291         this.xStart + (float)-Math.sin(angle) * this.offset,
292         this.yStart + (float)Math.cos(angle) * this.offset);
293     return containsShapeAtWithMargin(startExtensionLine, x, y, margin);
294   }
295 
296   /**
297    * Returns <code>true</code> if the extension line at the end of this dimension line
298    * contains the point at (<code>x</code>, <code>y</code>)
299    * with a given <code>margin</code> around the extension line.
300    */
containsEndExtensionLineAt(float x, float y, float margin)301   public boolean containsEndExtensionLineAt(float x, float y, float margin) {
302     double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
303     Line2D endExtensionLine = new Line2D.Float(this.xEnd, this.yEnd,
304         this.xEnd + (float)-Math.sin(angle) * this.offset,
305         this.yEnd + (float)Math.cos(angle) * this.offset);
306     return containsShapeAtWithMargin(endExtensionLine, x, y, margin);
307   }
308 
309   /**
310    * Returns <code>true</code> if <code>shape</code> contains
311    * the point at (<code>x</code>, <code>y</code>)
312    * with a given <code>margin</code>.
313    */
containsShapeAtWithMargin(Shape shape, float x, float y, float margin)314   private boolean containsShapeAtWithMargin(Shape shape, float x, float y, float margin) {
315     if (margin == 0) {
316       return shape.contains(x, y);
317     } else {
318       return shape.intersects(x - margin, y - margin, 2 * margin, 2 * margin);
319     }
320   }
321 
322   /**
323    * Returns the shape matching this dimension line.
324    */
getShape()325   private Shape getShape() {
326     if (this.shapeCache == null) {
327       // Create the rectangle that matches piece bounds
328       double angle = Math.atan2(this.yEnd - this.yStart, this.xEnd - this.xStart);
329       float dx = (float)-Math.sin(angle) * this.offset;
330       float dy = (float)Math.cos(angle) * this.offset;
331 
332       GeneralPath dimensionLineShape = new GeneralPath();
333       // Append dimension line
334       dimensionLineShape.append(new Line2D.Float(this.xStart + dx, this.yStart + dy, this.xEnd + dx, this.yEnd + dy), false);
335       // Append extension lines
336       dimensionLineShape.append(new Line2D.Float(this.xStart, this.yStart, this.xStart + dx, this.yStart + dy), false);
337       dimensionLineShape.append(new Line2D.Float(this.xEnd, this.yEnd, this.xEnd + dx, this.yEnd + dy), false);
338       // Cache shape
339       this.shapeCache = dimensionLineShape;
340     }
341     return this.shapeCache;
342   }
343 
344   /**
345    * Moves this dimension line of (<code>dx</code>, <code>dy</code>) units.
346    */
move(float dx, float dy)347   public void move(float dx, float dy) {
348     setXStart(getXStart() + dx);
349     setYStart(getYStart() + dy);
350     setXEnd(getXEnd() + dx);
351     setYEnd(getYEnd() + dy);
352   }
353 
354   /**
355    * Returns a clone of this dimension line.
356    */
357   @Override
clone()358   public DimensionLine clone() {
359     DimensionLine clone = (DimensionLine)super.clone();
360     clone.level = null;
361     return clone;
362   }
363 }
364