1 /*
2  * Polyline.java 17 juin 2015
3  *
4  * Sweet Home 3D, Copyright (c) 2015 Emmanuel PUYBARET / eTeks <info@eteks.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 package com.eteks.sweethome3d.model;
21 
22 import java.awt.BasicStroke;
23 import java.awt.Shape;
24 import java.awt.geom.CubicCurve2D;
25 import java.awt.geom.GeneralPath;
26 import java.awt.geom.PathIterator;
27 import java.awt.geom.Point2D;
28 import java.awt.geom.Rectangle2D;
29 import java.io.IOException;
30 import java.io.ObjectInputStream;
31 import java.util.Arrays;
32 
33 /**
34  * A polyline or a polygon in a home plan.
35  * @author Emmanuel Puybaret
36  * @since 5.0
37  */
38 public class Polyline extends HomeObject implements Selectable, Elevatable {
39   private static final long serialVersionUID = 1L;
40 
41   /**
42    * The properties of a polyline that may change. <code>PropertyChangeListener</code>s added
43    * to a polyline will be notified under a property name equal to the string value of one these properties.
44    */
45   public enum Property {POINTS, THICKNESS, CAP_STYLE, JOIN_STYLE, DASH_STYLE, DASH_OFFSET, DASH_PATTERN, START_ARROW_STYLE, END_ARROW_STYLE, CLOSED_PATH, COLOR, LEVEL, ELEVATION, VISIBLE_IN_3D}
46 
47   public enum CapStyle {BUTT, SQUARE, ROUND}
48 
49   public enum JoinStyle {BEVEL, MITER, ROUND, CURVED}
50 
51   public enum ArrowStyle {NONE, DELTA, OPEN, DISC}
52 
53   public enum DashStyle {SOLID, DOT, DASH, DASH_DOT, DASH_DOT_DOT, CUSTOMIZED;
54     /**
55      * Returns an array describing the length of dashes and spaces between them
56      * for a 1 cm thick polyline.
57      */
getDashPattern()58     public float [] getDashPattern() {
59       switch (this) {
60         case SOLID :        return new float [] {1f, 0f};
61         case DOT :          return new float [] {1f, 1f};
62         case DASH :         return new float [] {4f, 2f};
63         case DASH_DOT :     return new float [] {8f, 2f, 2f, 2f};
64         case DASH_DOT_DOT : return new float [] {8f, 2f, 2f, 2f, 2f, 2f};
65         default :           return null;
66       }
67     }
68   }
69 
70   private float [][]           points;
71   private float                thickness;
72   private transient CapStyle   capStyle;
73   private String               capStyleName;
74   private transient JoinStyle  joinStyle;
75   private String               joinStyleName;
76   private transient DashStyle  dashStyle;
77   private String               dashStyleName;
78   private float []             dashPattern;
79   private float                dashOffset;
80   private transient ArrowStyle startArrowStyle;
81   private String               startArrowStyleName;
82   private transient ArrowStyle endArrowStyle;
83   private String               endArrowStyleName;
84   private boolean              closedPath;
85   private int                  color;
86   private Float                elevation;
87   private Level                level;
88 
89   private transient Shape      polylinePathCache;
90   private transient Shape      shapeCache;
91 
92   /**
93    * Creates a polyline from the given coordinates.
94    */
Polyline(float [][] points)95   public Polyline(float [][] points) {
96     this(points, 1, CapStyle.BUTT, JoinStyle.MITER, DashStyle.SOLID, ArrowStyle.NONE, ArrowStyle.NONE, false, 0xFF000000);
97   }
98 
99   /**
100    * Creates a polyline from the given coordinates.
101    * @since 6.4
102    */
Polyline(String id, float [][] points)103   public Polyline(String id, float [][] points) {
104     this(id, points, 1, CapStyle.BUTT, JoinStyle.MITER, DashStyle.SOLID, 0f, ArrowStyle.NONE, ArrowStyle.NONE, false, 0xFF000000);
105   }
106 
107   /**
108    * Creates a polyline from the given coordinates.
109    */
Polyline(float [][] points, float thickness, CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle, ArrowStyle startArrowStyle, ArrowStyle endArrowStyle, boolean closedPath, int color)110   public Polyline(float [][] points, float thickness,
111                   CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle,
112                   ArrowStyle startArrowStyle, ArrowStyle endArrowStyle,
113                   boolean closedPath, int color) {
114     this(points, thickness, capStyle, joinStyle, dashStyle, 0f, startArrowStyle, endArrowStyle, closedPath, color);
115   }
116 
117   /**
118    * Creates a polyline from the given coordinates.
119    * @since 6.0
120    */
Polyline(float [][] points, float thickness, CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle, float dashOffset, ArrowStyle startArrowStyle, ArrowStyle endArrowStyle, boolean closedPath, int color)121   public Polyline(float [][] points, float thickness,
122                   CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle,
123                   float dashOffset, ArrowStyle startArrowStyle,
124                   ArrowStyle endArrowStyle, boolean closedPath, int color) {
125     this(createId("polyline"), points, thickness, capStyle, joinStyle, dashStyle, dashOffset,
126         startArrowStyle, endArrowStyle, closedPath, color);
127   }
128 
129   /**
130    * Creates a polyline from the given coordinates.
131    * @since 6.4
132    */
Polyline(String id, float [][] points, float thickness, CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle, float dashOffset, ArrowStyle startArrowStyle, ArrowStyle endArrowStyle, boolean closedPath, int color)133   public Polyline(String id, float [][] points, float thickness,
134                   CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle,
135                   float dashOffset, ArrowStyle startArrowStyle,
136                   ArrowStyle endArrowStyle, boolean closedPath, int color) {
137     super(id);
138     this.points = deepCopy(points);
139     this.thickness = thickness;
140     this.capStyle = capStyle;
141     this.joinStyle = joinStyle;
142     this.dashStyle = dashStyle;
143     this.dashOffset = dashOffset;
144     this.startArrowStyle = startArrowStyle;
145     this.endArrowStyle = endArrowStyle;
146     this.closedPath = closedPath;
147     this.color = color;
148   }
149 
150   /**
151    * Initializes polyline transient fields.
152    */
readObject(ObjectInputStream in)153   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
154     this.capStyle = CapStyle.BUTT;
155     this.joinStyle = JoinStyle.MITER;
156     this.dashStyle = DashStyle.SOLID;
157     this.startArrowStyle = ArrowStyle.NONE;
158     this.endArrowStyle = ArrowStyle.NONE;
159     in.defaultReadObject();
160     // Read styles from strings
161     try {
162       if (this.capStyleName != null) {
163         this.capStyle = CapStyle.valueOf(this.capStyleName);
164       }
165     } catch (IllegalArgumentException ex) {
166       // Ignore malformed enum constant
167     }
168     try {
169       if (this.joinStyleName != null) {
170         this.joinStyle = JoinStyle.valueOf(this.joinStyleName);
171       }
172     } catch (IllegalArgumentException ex) {
173       // Ignore malformed enum constant
174     }
175     try {
176       if (this.dashStyleName != null) {
177         this.dashStyle = DashStyle.valueOf(this.dashStyleName);
178       }
179     } catch (IllegalArgumentException ex) {
180       // Ignore malformed enum constant
181     }
182     try {
183       if (this.startArrowStyleName != null) {
184         this.startArrowStyle = ArrowStyle.valueOf(this.startArrowStyleName);
185       }
186     } catch (IllegalArgumentException ex) {
187       // Ignore malformed enum constant
188     }
189     try {
190       if (this.endArrowStyleName != null) {
191         this.endArrowStyle = ArrowStyle.valueOf(this.endArrowStyleName);
192       }
193     } catch (IllegalArgumentException ex) {
194       // Ignore malformed enum constant
195     }
196   }
197 
198   /**
199    * Returns the points of the polygon matching this polyline.
200    * @return an array of the (x,y) coordinates of the polyline points.
201    */
getPoints()202   public float [][] getPoints() {
203     return deepCopy(this.points);
204   }
205 
206   /**
207    * Returns the number of points of the polygon matching this polyline.
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 polyline. Once this polyline
223    * is updated, listeners added to this polyline 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 polyline.
233    */
updatePoints(float [][] points)234   private void updatePoints(float [][] points) {
235     float [][] oldPoints = this.points;
236     this.points = deepCopy(points);
237     this.polylinePathCache = null;
238     this.shapeCache = null;
239     firePropertyChange(Property.POINTS.name(), oldPoints, points);
240   }
241 
242   /**
243    * Adds a point at the end of polyline points.
244    */
addPoint(float x, float y)245   public void addPoint(float x, float y) {
246     addPoint(x, y, this.points.length);
247   }
248 
249   /**
250    * Adds a point at the given <code>index</code>.
251    * @throws IndexOutOfBoundsException if <code>index</code> is negative or > <code>getPointCount()</code>
252    */
addPoint(float x, float y, int index)253   public void addPoint(float x, float y, int index) {
254     if (index < 0 || index > this.points.length) {
255       throw new IndexOutOfBoundsException("Invalid index " + index);
256     }
257 
258     float [][] newPoints = new float [this.points.length + 1][];
259     System.arraycopy(this.points, 0, newPoints, 0, index);
260     newPoints [index] = new float [] {x, y};
261     System.arraycopy(this.points, index, newPoints, index + 1, this.points.length - index);
262 
263     float [][] oldPoints = this.points;
264     this.points = newPoints;
265     this.polylinePathCache = null;
266     this.shapeCache = null;
267     firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
268   }
269 
270   /**
271    * Sets the point at the given <code>index</code>.
272    * @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
273    */
setPoint(float x, float y, int index)274   public void setPoint(float x, float y, int index) {
275     if (index < 0 || index >= this.points.length) {
276       throw new IndexOutOfBoundsException("Invalid index " + index);
277     }
278     if (this.points [index][0] != x
279         || this.points [index][1] != y) {
280       float [][] oldPoints = this.points;
281       this.points = deepCopy(this.points);
282       this.points [index][0] = x;
283       this.points [index][1] = y;
284       this.polylinePathCache = null;
285       this.shapeCache = null;
286       firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
287     }
288   }
289 
290   /**
291    * Removes the point at the given <code>index</code>.
292    * @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
293    */
removePoint(int index)294   public void removePoint(int index) {
295     if (index < 0 || index >= this.points.length) {
296       throw new IndexOutOfBoundsException("Invalid index " + index);
297     }
298 
299     float [][] newPoints = new float [this.points.length - 1][];
300     System.arraycopy(this.points, 0, newPoints, 0, index);
301     System.arraycopy(this.points, index + 1, newPoints, index, this.points.length - index - 1);
302 
303     float [][] oldPoints = this.points;
304     this.points = newPoints;
305     this.polylinePathCache = null;
306     this.shapeCache = null;
307     firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
308   }
309 
310   /**
311    * Returns the thickness of this polyline.
312    */
getThickness()313   public float getThickness() {
314     return this.thickness;
315   }
316 
317   /**
318    * Sets the thickness of this polyline.
319    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
320    */
setThickness(float thickness)321   public void setThickness(float thickness) {
322     if (thickness != this.thickness) {
323       float oldThickness = this.thickness;
324       this.thickness = thickness;
325       firePropertyChange(Property.THICKNESS.name(), oldThickness, thickness);
326     }
327   }
328 
329   /**
330    * Returns the cap style of this polyline.
331    */
getCapStyle()332   public CapStyle getCapStyle() {
333     return this.capStyle;
334   }
335 
336   /**
337    * Sets the cap style of this polyline.
338    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
339    */
setCapStyle(CapStyle capStyle)340   public void setCapStyle(CapStyle capStyle) {
341     if (capStyle != this.capStyle) {
342       CapStyle oldStyle = this.capStyle;
343       this.capStyle = capStyle;
344       this.capStyleName = this.capStyle.name();
345       firePropertyChange(Property.CAP_STYLE.name(), oldStyle, capStyle);
346     }
347   }
348 
349   /**
350    * Returns the join style of this polyline.
351    */
getJoinStyle()352   public JoinStyle getJoinStyle() {
353     return this.joinStyle;
354   }
355 
356   /**
357    * Sets the join style of this polyline.
358    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
359    */
setJoinStyle(JoinStyle joinStyle)360   public void setJoinStyle(JoinStyle joinStyle) {
361     if (joinStyle != this.joinStyle) {
362       JoinStyle oldJoinStyle = this.joinStyle;
363       this.joinStyle = joinStyle;
364       this.joinStyleName = this.joinStyle.name();
365       this.polylinePathCache = null;
366       this.shapeCache = null;
367       firePropertyChange(Property.JOIN_STYLE.name(), oldJoinStyle, joinStyle);
368     }
369   }
370 
371   /**
372    * Returns the dash style of this polyline. If <code>DashStyle.CUSTOMIZED</code> is returned,
373    * the actual dash pattern will be returned by {@link #getDashPattern()}.
374    */
getDashStyle()375   public DashStyle getDashStyle() {
376     return this.dashStyle;
377   }
378 
379   /**
380    * Sets the dash style of this polyline.
381    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
382    */
setDashStyle(DashStyle dashStyle)383   public void setDashStyle(DashStyle dashStyle) {
384     if (dashStyle != this.dashStyle) {
385       float [] oldDashPattern = getDashPattern();
386       DashStyle oldDashStyle = this.dashStyle;
387       this.dashStyle = dashStyle;
388       this.dashStyleName = this.dashStyle.name();
389       if (dashStyle != DashStyle.CUSTOMIZED) {
390         this.dashPattern = null;
391       }
392       firePropertyChange(Property.DASH_PATTERN.name(), oldDashPattern, getDashPattern());
393       firePropertyChange(Property.DASH_STYLE.name(), oldDashStyle, dashStyle);
394     }
395   }
396 
397   /**
398    * Returns the dash pattern of this polyline in percentage of its thickness.
399    * @since 6.0
400    */
getDashPattern()401   public float [] getDashPattern() {
402     float [] dashPattern = null;
403     if (this.dashStyle != DashStyle.CUSTOMIZED) {
404       return this.dashStyle.getDashPattern();
405     } else if (this.dashPattern != null) {
406       dashPattern = this.dashPattern.clone();
407     }
408     return dashPattern;
409   }
410 
411   /**
412    * Sets the dash pattern of this polyline in percentage of its thickness.
413    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
414    * @since 6.0
415    */
setDashPattern(float [] dashPattern)416   public void setDashPattern(float [] dashPattern) {
417     for (DashStyle dashStyle : DashStyle.values()) {
418       if (this.dashStyle != DashStyle.CUSTOMIZED) {
419         // If the given dash pattern matches a default dash style, simply set this dash style
420         if (Arrays.equals(dashPattern, dashStyle.getDashPattern())) {
421           setDashStyle(dashStyle);
422           return;
423         }
424       }
425     }
426     if (!Arrays.equals(dashPattern, this.dashPattern)) {
427       float [] oldDashPattern = getDashPattern();
428       this.dashPattern = dashPattern.clone();
429       firePropertyChange(Property.DASH_PATTERN.name(), oldDashPattern, dashPattern);
430       // Always emit a DASH_STYLE change to let existing listeners know that the pattern change
431       DashStyle oldDashStyle = this.dashStyle;
432       this.dashStyle = DashStyle.CUSTOMIZED;
433       this.dashStyleName = this.dashStyle.name();
434       firePropertyChange(Property.DASH_STYLE.name(), oldDashStyle, DashStyle.CUSTOMIZED);
435     }
436   }
437 
438   /**
439    * Returns the offset from which the dash of this polyline should start.
440    * @return the offset in percentage of the dash pattern
441    * @since 6.0
442    */
getDashOffset()443   public float getDashOffset() {
444     return this.dashOffset;
445   }
446 
447   /**
448    * Sets the offset from which the dash of this polyline should start.
449    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
450    * @param dashOffset the offset in percentage of the dash pattern
451    * @since 6.0
452    */
setDashOffset(float dashOffset)453   public void setDashOffset(float dashOffset) {
454     if (dashOffset != this.dashOffset) {
455       float oldDashOffset = this.dashOffset;
456       this.dashOffset = dashOffset;
457       firePropertyChange(Property.DASH_OFFSET.name(), oldDashOffset, dashOffset);
458     }
459   }
460 
461   /**
462    * Returns the arrow style at the start of this polyline.
463    */
getStartArrowStyle()464   public ArrowStyle getStartArrowStyle() {
465     return this.startArrowStyle;
466   }
467 
468   /**
469    * Sets the arrow style at the start of this polyline.
470    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
471    */
setStartArrowStyle(ArrowStyle startArrowStyle)472   public void setStartArrowStyle(ArrowStyle startArrowStyle) {
473     if (startArrowStyle != this.startArrowStyle) {
474       ArrowStyle oldStartArrowStyle = this.startArrowStyle;
475       this.startArrowStyle = startArrowStyle;
476       this.startArrowStyleName = this.startArrowStyle.name();
477       firePropertyChange(Property.START_ARROW_STYLE.name(), oldStartArrowStyle, startArrowStyle);
478     }
479   }
480 
481   /**
482    * Returns the arrow style at the end of this polyline.
483    */
getEndArrowStyle()484   public ArrowStyle getEndArrowStyle() {
485     return this.endArrowStyle;
486   }
487 
488   /**
489    * Sets the arrow style at the end of this polyline.
490    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
491    */
setEndArrowStyle(ArrowStyle endArrowStyle)492   public void setEndArrowStyle(ArrowStyle endArrowStyle) {
493     if (endArrowStyle != this.endArrowStyle) {
494       ArrowStyle oldEndArrowStyle = this.endArrowStyle;
495       this.endArrowStyle = endArrowStyle;
496       this.endArrowStyleName = this.endArrowStyle.name();
497       firePropertyChange(Property.END_ARROW_STYLE.name(), oldEndArrowStyle, endArrowStyle);
498     }
499   }
500 
501   /**
502    * Returns <code>true</code> if the first and last points of this polyline should be joined to form a polygon.
503    */
isClosedPath()504   public boolean isClosedPath() {
505     return this.closedPath;
506   }
507 
508   /**
509    * Sets whether the first and last points of this polyline should be joined.
510    * Once this polyline is updated, listeners added to this polyline will receive a change notification.
511    */
setClosedPath(boolean closedPath)512   public void setClosedPath(boolean closedPath) {
513     if (closedPath != this.closedPath) {
514       this.closedPath = closedPath;
515       firePropertyChange(Property.CLOSED_PATH.name(), !closedPath, closedPath);
516     }
517   }
518 
519   /**
520    * Returns the color of this polyline.
521    */
getColor()522   public int getColor() {
523     return this.color;
524   }
525 
526   /**
527    * Sets the color of this polyline. Once this polyline is updated,
528    * listeners added to this polyline will receive a change notification.
529    */
setColor(int color)530   public void setColor(int color) {
531     if (color != this.color) {
532       int oldColor = this.color;
533       this.color = color;
534       firePropertyChange(Property.COLOR.name(), oldColor, color);
535     }
536   }
537 
538   /**
539    * Returns the elevation of this polyline
540    * from the ground according to the elevation of its level.
541    * @since 6.0
542    */
getGroundElevation()543   public float getGroundElevation() {
544     float elevation = getElevation();
545     if (this.level != null) {
546       return elevation + this.level.getElevation();
547     } else {
548       return elevation;
549     }
550   }
551 
552   /**
553    * Returns the elevation of this polyline in 3D.
554    * @since 6.0
555    */
getElevation()556   public float getElevation() {
557     return this.elevation != null ? this.elevation : 0;
558   }
559 
560   /**
561    * Sets the elevation of this polyline in 3D. Once this polyline is updated,
562    * listeners added to this polyline will receive a change notification.
563    * @since 6.0
564    */
setElevation(float elevation)565   public void setElevation(float elevation) {
566     if (this.elevation != null && elevation != this.elevation) {
567       float oldElevation = this.elevation;
568       this.elevation = elevation;
569       firePropertyChange(Property.ELEVATION.name(), oldElevation, elevation);
570     }
571   }
572 
573   /**
574    * Returns <code>true</code> if this polyline should be displayed in 3D.
575    * @since 6.0
576    */
isVisibleIn3D()577   public boolean isVisibleIn3D() {
578     return this.elevation != null;
579   }
580 
581   /**
582    * Sets whether this polyline should be displayed in 3D and fires a <code>PropertyChangeEvent</code>.
583    * @since 6.0
584    */
setVisibleIn3D(boolean visibleIn3D)585   public void setVisibleIn3D(boolean visibleIn3D) {
586     if (visibleIn3D ^ (this.elevation != null)) {
587       this.elevation = visibleIn3D ? Float.valueOf(0) : null;
588       firePropertyChange(Property.VISIBLE_IN_3D.name(), !visibleIn3D, visibleIn3D);
589     }
590   }
591 
592   /**
593    * Returns the level which this polyline belongs to.
594    */
getLevel()595   public Level getLevel() {
596     return this.level;
597   }
598 
599   /**
600    * Sets the level of this polyline. Once this polyline is updated,
601    * listeners added to this polyline will receive a change notification.
602    */
setLevel(Level level)603   public void setLevel(Level level) {
604     if (level != this.level) {
605       Level oldLevel = this.level;
606       this.level = level;
607       firePropertyChange(Property.LEVEL.name(), oldLevel, level);
608     }
609   }
610 
611   /**
612    * Returns <code>true</code> if this polyline is at the given <code>level</code>
613    * or at a level with the same elevation and a smaller elevation index.
614    */
isAtLevel(Level level)615   public boolean isAtLevel(Level level) {
616     return this.level == level
617         || this.level != null && level != null
618            && this.level.getElevation() == level.getElevation()
619            && this.level.getElevationIndex() < level.getElevationIndex();
620 
621   }
622 
623   /**
624    * Returns an approximate length of this polyline.
625    */
getLength()626   public float getLength() {
627     float [] firstPoint = new float [2];
628     float [] previousPoint = new float [2];
629     float [] point = new float [2];
630     float length = 0;
631     for (PathIterator it = getPolylinePath().getPathIterator(null, 0.1); !it.isDone(); it.next()) {
632       switch (it.currentSegment(point)) {
633         case PathIterator.SEG_CLOSE :
634           length += Point2D.distance(firstPoint [0], firstPoint [1], previousPoint [0], previousPoint [1]);
635           break;
636         case PathIterator.SEG_MOVETO :
637           System.arraycopy(point, 0, firstPoint, 0, 2);
638           System.arraycopy(point, 0, previousPoint, 0, 2);
639           break;
640         case PathIterator.SEG_LINETO :
641           length += Point2D.distance(previousPoint [0], previousPoint [1], point [0], point [1]);
642           System.arraycopy(point, 0, previousPoint, 0, 2);
643           break;
644       }
645     }
646     return length;
647   }
648 
649   /**
650    * Returns <code>true</code> if this polyline intersects
651    * with the horizontal rectangle which opposite corners are at points
652    * (<code>x0</code>, <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
653    */
intersectsRectangle(float x0, float y0, float x1, float y1)654   public boolean intersectsRectangle(float x0, float y0, float x1, float y1) {
655     Rectangle2D rectangle = new Rectangle2D.Float(x0, y0, 0, 0);
656     rectangle.add(x1, y1);
657     return getShape().intersects(rectangle);
658   }
659 
660   /**
661    * Returns <code>true</code> if this polyline contains
662    * the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
663    */
containsPoint(float x, float y, float margin)664   public boolean containsPoint(float x, float y, float margin) {
665     return containsShapeAtWithMargin(getShape(), x, y, margin);
666   }
667 
668   /**
669    * Returns the index of the point of this polyline equal to
670    * the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
671    * @return the index of the first found point or -1.
672    */
getPointIndexAt(float x, float y, float margin)673   public int getPointIndexAt(float x, float y, float margin) {
674     for (int i = 0; i < this.points.length; i++) {
675       if (Math.abs(x - this.points [i][0]) <= margin && Math.abs(y - this.points [i][1]) <= margin) {
676         return i;
677       }
678     }
679     return -1;
680   }
681 
682   /**
683    * Returns <code>true</code> if <code>shape</code> contains
684    * the point at (<code>x</code>, <code>y</code>)
685    * with a given <code>margin</code>.
686    */
containsShapeAtWithMargin(Shape shape, float x, float y, float margin)687   private boolean containsShapeAtWithMargin(Shape shape, float x, float y, float margin) {
688     if (margin == 0) {
689       return shape.contains(x, y);
690     } else {
691       return shape.intersects(x - margin, y - margin, 2 * margin, 2 * margin);
692     }
693   }
694 
695   /**
696    * Returns the path matching this polyline.
697    */
getPolylinePath()698   private Shape getPolylinePath() {
699     if (this.polylinePathCache == null) {
700       GeneralPath polylinePath = new GeneralPath();
701       if (this.joinStyle == JoinStyle.CURVED) {
702         for (int i = 0, n = this.closedPath ? this.points.length : this.points.length - 1; i < n; i++) {
703           CubicCurve2D.Float curve2D = new CubicCurve2D.Float();
704           float [] previousPoint = this.points [i == 0 ?  this.points.length - 1  : i - 1];
705           float [] point         = this.points [i];
706           float [] nextPoint     = this.points [i == this.points.length - 1 ?  0  : i + 1];
707           float [] vectorToBisectorPoint = new float [] {nextPoint [0] - previousPoint [0], nextPoint [1] - previousPoint [1]};
708           float [] nextNextPoint     = this.points [(i + 2) % this.points.length];
709           float [] vectorToBisectorNextPoint = new float [] {point[0] - nextNextPoint [0], point[1] - nextNextPoint [1]};
710           curve2D.setCurve(point[0], point[1],
711               point [0] + (i != 0 || this.closedPath  ? vectorToBisectorPoint [0] / 3.625f  : 0),
712               point [1] + (i != 0 || this.closedPath  ? vectorToBisectorPoint [1] / 3.625f  : 0),
713               nextPoint [0] + (i != this.points.length - 2 || this.closedPath  ? vectorToBisectorNextPoint [0] / 3.625f  : 0),
714               nextPoint [1] + (i != this.points.length - 2 || this.closedPath  ? vectorToBisectorNextPoint [1] / 3.625f  : 0),
715               nextPoint [0], nextPoint [1]);
716           polylinePath.append(curve2D, true);
717         }
718       } else {
719         polylinePath.moveTo(this.points [0][0], this.points [0][1]);
720         for (int i = 1; i < this.points.length; i++) {
721           polylinePath.lineTo(this.points [i][0], this.points [i][1]);
722         }
723         if (this.closedPath) {
724           polylinePath.closePath();
725         }
726       }
727       // Cache polylineShape
728       this.polylinePathCache = polylinePath;
729     }
730     return this.polylinePathCache;
731   }
732 
733   /**
734    * Returns the shape matching this polyline.
735    */
getShape()736   private Shape getShape() {
737     if (this.shapeCache == null) {
738       this.shapeCache = new BasicStroke(this.thickness).createStrokedShape(getPolylinePath());
739     }
740     return this.shapeCache;
741   }
742 
743   /**
744    * Moves this polyline of (<code>dx</code>, <code>dy</code>) units.
745    */
move(float dx, float dy)746   public void move(float dx, float dy) {
747     if (dx != 0 || dy != 0) {
748       float [][] points = getPoints();
749       for (int i = 0; i < points.length; i++) {
750         points [i][0] += dx;
751         points [i][1] += dy;
752       }
753       updatePoints(points);
754     }
755   }
756 
757   /**
758    * Returns a clone of this polyline.
759    */
760   @Override
clone()761   public Polyline clone() {
762     Polyline clone = (Polyline)super.clone();
763     clone.level = null;
764     return clone;
765   }
766 }
767