/*
 * Polyline.java 17 juin 2015
 *
 * Sweet Home 3D, Copyright (c) 2015 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.model;

import java.awt.BasicStroke;
import java.awt.Shape;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Arrays;

/**
 * A polyline or a polygon in a home plan.
 * @author Emmanuel Puybaret
 * @since 5.0
 */
public class Polyline extends HomeObject implements Selectable, Elevatable {
  private static final long serialVersionUID = 1L;

  /**
   * The properties of a polyline that may change. <code>PropertyChangeListener</code>s added
   * to a polyline will be notified under a property name equal to the string value of one these properties.
   */
  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}

  public enum CapStyle {BUTT, SQUARE, ROUND}

  public enum JoinStyle {BEVEL, MITER, ROUND, CURVED}

  public enum ArrowStyle {NONE, DELTA, OPEN, DISC}

  public enum DashStyle {SOLID, DOT, DASH, DASH_DOT, DASH_DOT_DOT, CUSTOMIZED;
    /**
     * Returns an array describing the length of dashes and spaces between them
     * for a 1 cm thick polyline.
     */
    public float [] getDashPattern() {
      switch (this) {
        case SOLID :        return new float [] {1f, 0f};
        case DOT :          return new float [] {1f, 1f};
        case DASH :         return new float [] {4f, 2f};
        case DASH_DOT :     return new float [] {8f, 2f, 2f, 2f};
        case DASH_DOT_DOT : return new float [] {8f, 2f, 2f, 2f, 2f, 2f};
        default :           return null;
      }
    }
  }

  private float [][]           points;
  private float                thickness;
  private transient CapStyle   capStyle;
  private String               capStyleName;
  private transient JoinStyle  joinStyle;
  private String               joinStyleName;
  private transient DashStyle  dashStyle;
  private String               dashStyleName;
  private float []             dashPattern;
  private float                dashOffset;
  private transient ArrowStyle startArrowStyle;
  private String               startArrowStyleName;
  private transient ArrowStyle endArrowStyle;
  private String               endArrowStyleName;
  private boolean              closedPath;
  private int                  color;
  private Float                elevation;
  private Level                level;

  private transient Shape      polylinePathCache;
  private transient Shape      shapeCache;

  /**
   * Creates a polyline from the given coordinates.
   */
  public Polyline(float [][] points) {
    this(points, 1, CapStyle.BUTT, JoinStyle.MITER, DashStyle.SOLID, ArrowStyle.NONE, ArrowStyle.NONE, false, 0xFF000000);
  }

  /**
   * Creates a polyline from the given coordinates.
   * @since 6.4
   */
  public Polyline(String id, float [][] points) {
    this(id, points, 1, CapStyle.BUTT, JoinStyle.MITER, DashStyle.SOLID, 0f, ArrowStyle.NONE, ArrowStyle.NONE, false, 0xFF000000);
  }

  /**
   * Creates a polyline from the given coordinates.
   */
  public Polyline(float [][] points, float thickness,
                  CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle,
                  ArrowStyle startArrowStyle, ArrowStyle endArrowStyle,
                  boolean closedPath, int color) {
    this(points, thickness, capStyle, joinStyle, dashStyle, 0f, startArrowStyle, endArrowStyle, closedPath, color);
  }

  /**
   * Creates a polyline from the given coordinates.
   * @since 6.0
   */
  public Polyline(float [][] points, float thickness,
                  CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle,
                  float dashOffset, ArrowStyle startArrowStyle,
                  ArrowStyle endArrowStyle, boolean closedPath, int color) {
    this(createId("polyline"), points, thickness, capStyle, joinStyle, dashStyle, dashOffset,
        startArrowStyle, endArrowStyle, closedPath, color);
  }

  /**
   * Creates a polyline from the given coordinates.
   * @since 6.4
   */
  public Polyline(String id, float [][] points, float thickness,
                  CapStyle capStyle, JoinStyle joinStyle, DashStyle dashStyle,
                  float dashOffset, ArrowStyle startArrowStyle,
                  ArrowStyle endArrowStyle, boolean closedPath, int color) {
    super(id);
    this.points = deepCopy(points);
    this.thickness = thickness;
    this.capStyle = capStyle;
    this.joinStyle = joinStyle;
    this.dashStyle = dashStyle;
    this.dashOffset = dashOffset;
    this.startArrowStyle = startArrowStyle;
    this.endArrowStyle = endArrowStyle;
    this.closedPath = closedPath;
    this.color = color;
  }

  /**
   * Initializes polyline transient fields.
   */
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    this.capStyle = CapStyle.BUTT;
    this.joinStyle = JoinStyle.MITER;
    this.dashStyle = DashStyle.SOLID;
    this.startArrowStyle = ArrowStyle.NONE;
    this.endArrowStyle = ArrowStyle.NONE;
    in.defaultReadObject();
    // Read styles from strings
    try {
      if (this.capStyleName != null) {
        this.capStyle = CapStyle.valueOf(this.capStyleName);
      }
    } catch (IllegalArgumentException ex) {
      // Ignore malformed enum constant
    }
    try {
      if (this.joinStyleName != null) {
        this.joinStyle = JoinStyle.valueOf(this.joinStyleName);
      }
    } catch (IllegalArgumentException ex) {
      // Ignore malformed enum constant
    }
    try {
      if (this.dashStyleName != null) {
        this.dashStyle = DashStyle.valueOf(this.dashStyleName);
      }
    } catch (IllegalArgumentException ex) {
      // Ignore malformed enum constant
    }
    try {
      if (this.startArrowStyleName != null) {
        this.startArrowStyle = ArrowStyle.valueOf(this.startArrowStyleName);
      }
    } catch (IllegalArgumentException ex) {
      // Ignore malformed enum constant
    }
    try {
      if (this.endArrowStyleName != null) {
        this.endArrowStyle = ArrowStyle.valueOf(this.endArrowStyleName);
      }
    } catch (IllegalArgumentException ex) {
      // Ignore malformed enum constant
    }
  }

  /**
   * Returns the points of the polygon matching this polyline.
   * @return an array of the (x,y) coordinates of the polyline points.
   */
  public float [][] getPoints() {
    return deepCopy(this.points);
  }

  /**
   * Returns the number of points of the polygon matching this polyline.
   */
  public int getPointCount() {
    return this.points.length;
  }

  private float [][] deepCopy(float [][] points) {
    float [][] pointsCopy = new float [points.length][];
    for (int i = 0; i < points.length; i++) {
      pointsCopy [i] = points [i].clone();
    }
    return pointsCopy;
  }

  /**
   * Sets the points of the polygon matching this polyline. Once this polyline
   * is updated, listeners added to this polyline will receive a change notification.
   */
  public void setPoints(float [][] points) {
    if (!Arrays.deepEquals(this.points, points)) {
      updatePoints(points);
    }
  }

  /**
   * Update the points of the polygon matching this polyline.
   */
  private void updatePoints(float [][] points) {
    float [][] oldPoints = this.points;
    this.points = deepCopy(points);
    this.polylinePathCache = null;
    this.shapeCache = null;
    firePropertyChange(Property.POINTS.name(), oldPoints, points);
  }

  /**
   * Adds a point at the end of polyline points.
   */
  public void addPoint(float x, float y) {
    addPoint(x, y, this.points.length);
  }

  /**
   * Adds a point at the given <code>index</code>.
   * @throws IndexOutOfBoundsException if <code>index</code> is negative or > <code>getPointCount()</code>
   */
  public void addPoint(float x, float y, int index) {
    if (index < 0 || index > this.points.length) {
      throw new IndexOutOfBoundsException("Invalid index " + index);
    }

    float [][] newPoints = new float [this.points.length + 1][];
    System.arraycopy(this.points, 0, newPoints, 0, index);
    newPoints [index] = new float [] {x, y};
    System.arraycopy(this.points, index, newPoints, index + 1, this.points.length - index);

    float [][] oldPoints = this.points;
    this.points = newPoints;
    this.polylinePathCache = null;
    this.shapeCache = null;
    firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
  }

  /**
   * Sets the point at the given <code>index</code>.
   * @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
   */
  public void setPoint(float x, float y, int index) {
    if (index < 0 || index >= this.points.length) {
      throw new IndexOutOfBoundsException("Invalid index " + index);
    }
    if (this.points [index][0] != x
        || this.points [index][1] != y) {
      float [][] oldPoints = this.points;
      this.points = deepCopy(this.points);
      this.points [index][0] = x;
      this.points [index][1] = y;
      this.polylinePathCache = null;
      this.shapeCache = null;
      firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
    }
  }

  /**
   * Removes the point at the given <code>index</code>.
   * @throws IndexOutOfBoundsException if <code>index</code> is negative or >= <code>getPointCount()</code>
   */
  public void removePoint(int index) {
    if (index < 0 || index >= this.points.length) {
      throw new IndexOutOfBoundsException("Invalid index " + index);
    }

    float [][] newPoints = new float [this.points.length - 1][];
    System.arraycopy(this.points, 0, newPoints, 0, index);
    System.arraycopy(this.points, index + 1, newPoints, index, this.points.length - index - 1);

    float [][] oldPoints = this.points;
    this.points = newPoints;
    this.polylinePathCache = null;
    this.shapeCache = null;
    firePropertyChange(Property.POINTS.name(), oldPoints, deepCopy(this.points));
  }

  /**
   * Returns the thickness of this polyline.
   */
  public float getThickness() {
    return this.thickness;
  }

  /**
   * Sets the thickness of this polyline.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   */
  public void setThickness(float thickness) {
    if (thickness != this.thickness) {
      float oldThickness = this.thickness;
      this.thickness = thickness;
      firePropertyChange(Property.THICKNESS.name(), oldThickness, thickness);
    }
  }

  /**
   * Returns the cap style of this polyline.
   */
  public CapStyle getCapStyle() {
    return this.capStyle;
  }

  /**
   * Sets the cap style of this polyline.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   */
  public void setCapStyle(CapStyle capStyle) {
    if (capStyle != this.capStyle) {
      CapStyle oldStyle = this.capStyle;
      this.capStyle = capStyle;
      this.capStyleName = this.capStyle.name();
      firePropertyChange(Property.CAP_STYLE.name(), oldStyle, capStyle);
    }
  }

  /**
   * Returns the join style of this polyline.
   */
  public JoinStyle getJoinStyle() {
    return this.joinStyle;
  }

  /**
   * Sets the join style of this polyline.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   */
  public void setJoinStyle(JoinStyle joinStyle) {
    if (joinStyle != this.joinStyle) {
      JoinStyle oldJoinStyle = this.joinStyle;
      this.joinStyle = joinStyle;
      this.joinStyleName = this.joinStyle.name();
      this.polylinePathCache = null;
      this.shapeCache = null;
      firePropertyChange(Property.JOIN_STYLE.name(), oldJoinStyle, joinStyle);
    }
  }

  /**
   * Returns the dash style of this polyline. If <code>DashStyle.CUSTOMIZED</code> is returned,
   * the actual dash pattern will be returned by {@link #getDashPattern()}.
   */
  public DashStyle getDashStyle() {
    return this.dashStyle;
  }

  /**
   * Sets the dash style of this polyline.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   */
  public void setDashStyle(DashStyle dashStyle) {
    if (dashStyle != this.dashStyle) {
      float [] oldDashPattern = getDashPattern();
      DashStyle oldDashStyle = this.dashStyle;
      this.dashStyle = dashStyle;
      this.dashStyleName = this.dashStyle.name();
      if (dashStyle != DashStyle.CUSTOMIZED) {
        this.dashPattern = null;
      }
      firePropertyChange(Property.DASH_PATTERN.name(), oldDashPattern, getDashPattern());
      firePropertyChange(Property.DASH_STYLE.name(), oldDashStyle, dashStyle);
    }
  }

  /**
   * Returns the dash pattern of this polyline in percentage of its thickness.
   * @since 6.0
   */
  public float [] getDashPattern() {
    float [] dashPattern = null;
    if (this.dashStyle != DashStyle.CUSTOMIZED) {
      return this.dashStyle.getDashPattern();
    } else if (this.dashPattern != null) {
      dashPattern = this.dashPattern.clone();
    }
    return dashPattern;
  }

  /**
   * Sets the dash pattern of this polyline in percentage of its thickness.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   * @since 6.0
   */
  public void setDashPattern(float [] dashPattern) {
    for (DashStyle dashStyle : DashStyle.values()) {
      if (this.dashStyle != DashStyle.CUSTOMIZED) {
        // If the given dash pattern matches a default dash style, simply set this dash style
        if (Arrays.equals(dashPattern, dashStyle.getDashPattern())) {
          setDashStyle(dashStyle);
          return;
        }
      }
    }
    if (!Arrays.equals(dashPattern, this.dashPattern)) {
      float [] oldDashPattern = getDashPattern();
      this.dashPattern = dashPattern.clone();
      firePropertyChange(Property.DASH_PATTERN.name(), oldDashPattern, dashPattern);
      // Always emit a DASH_STYLE change to let existing listeners know that the pattern change
      DashStyle oldDashStyle = this.dashStyle;
      this.dashStyle = DashStyle.CUSTOMIZED;
      this.dashStyleName = this.dashStyle.name();
      firePropertyChange(Property.DASH_STYLE.name(), oldDashStyle, DashStyle.CUSTOMIZED);
    }
  }

  /**
   * Returns the offset from which the dash of this polyline should start.
   * @return the offset in percentage of the dash pattern
   * @since 6.0
   */
  public float getDashOffset() {
    return this.dashOffset;
  }

  /**
   * Sets the offset from which the dash of this polyline should start.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   * @param dashOffset the offset in percentage of the dash pattern
   * @since 6.0
   */
  public void setDashOffset(float dashOffset) {
    if (dashOffset != this.dashOffset) {
      float oldDashOffset = this.dashOffset;
      this.dashOffset = dashOffset;
      firePropertyChange(Property.DASH_OFFSET.name(), oldDashOffset, dashOffset);
    }
  }

  /**
   * Returns the arrow style at the start of this polyline.
   */
  public ArrowStyle getStartArrowStyle() {
    return this.startArrowStyle;
  }

  /**
   * Sets the arrow style at the start of this polyline.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   */
  public void setStartArrowStyle(ArrowStyle startArrowStyle) {
    if (startArrowStyle != this.startArrowStyle) {
      ArrowStyle oldStartArrowStyle = this.startArrowStyle;
      this.startArrowStyle = startArrowStyle;
      this.startArrowStyleName = this.startArrowStyle.name();
      firePropertyChange(Property.START_ARROW_STYLE.name(), oldStartArrowStyle, startArrowStyle);
    }
  }

  /**
   * Returns the arrow style at the end of this polyline.
   */
  public ArrowStyle getEndArrowStyle() {
    return this.endArrowStyle;
  }

  /**
   * Sets the arrow style at the end of this polyline.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   */
  public void setEndArrowStyle(ArrowStyle endArrowStyle) {
    if (endArrowStyle != this.endArrowStyle) {
      ArrowStyle oldEndArrowStyle = this.endArrowStyle;
      this.endArrowStyle = endArrowStyle;
      this.endArrowStyleName = this.endArrowStyle.name();
      firePropertyChange(Property.END_ARROW_STYLE.name(), oldEndArrowStyle, endArrowStyle);
    }
  }

  /**
   * Returns <code>true</code> if the first and last points of this polyline should be joined to form a polygon.
   */
  public boolean isClosedPath() {
    return this.closedPath;
  }

  /**
   * Sets whether the first and last points of this polyline should be joined.
   * Once this polyline is updated, listeners added to this polyline will receive a change notification.
   */
  public void setClosedPath(boolean closedPath) {
    if (closedPath != this.closedPath) {
      this.closedPath = closedPath;
      firePropertyChange(Property.CLOSED_PATH.name(), !closedPath, closedPath);
    }
  }

  /**
   * Returns the color of this polyline.
   */
  public int getColor() {
    return this.color;
  }

  /**
   * Sets the color of this polyline. Once this polyline is updated,
   * listeners added to this polyline will receive a change notification.
   */
  public void setColor(int color) {
    if (color != this.color) {
      int oldColor = this.color;
      this.color = color;
      firePropertyChange(Property.COLOR.name(), oldColor, color);
    }
  }

  /**
   * Returns the elevation of this polyline
   * from the ground according to the elevation of its level.
   * @since 6.0
   */
  public float getGroundElevation() {
    float elevation = getElevation();
    if (this.level != null) {
      return elevation + this.level.getElevation();
    } else {
      return elevation;
    }
  }

  /**
   * Returns the elevation of this polyline in 3D.
   * @since 6.0
   */
  public float getElevation() {
    return this.elevation != null ? this.elevation : 0;
  }

  /**
   * Sets the elevation of this polyline in 3D. Once this polyline is updated,
   * listeners added to this polyline will receive a change notification.
   * @since 6.0
   */
  public void setElevation(float elevation) {
    if (this.elevation != null && elevation != this.elevation) {
      float oldElevation = this.elevation;
      this.elevation = elevation;
      firePropertyChange(Property.ELEVATION.name(), oldElevation, elevation);
    }
  }

  /**
   * Returns <code>true</code> if this polyline should be displayed in 3D.
   * @since 6.0
   */
  public boolean isVisibleIn3D() {
    return this.elevation != null;
  }

  /**
   * Sets whether this polyline should be displayed in 3D and fires a <code>PropertyChangeEvent</code>.
   * @since 6.0
   */
  public void setVisibleIn3D(boolean visibleIn3D) {
    if (visibleIn3D ^ (this.elevation != null)) {
      this.elevation = visibleIn3D ? Float.valueOf(0) : null;
      firePropertyChange(Property.VISIBLE_IN_3D.name(), !visibleIn3D, visibleIn3D);
    }
  }

  /**
   * Returns the level which this polyline belongs to.
   */
  public Level getLevel() {
    return this.level;
  }

  /**
   * Sets the level of this polyline. Once this polyline is updated,
   * listeners added to this polyline will receive a change notification.
   */
  public void setLevel(Level level) {
    if (level != this.level) {
      Level oldLevel = this.level;
      this.level = level;
      firePropertyChange(Property.LEVEL.name(), oldLevel, level);
    }
  }

  /**
   * Returns <code>true</code> if this polyline is at the given <code>level</code>
   * or at a level with the same elevation and a smaller elevation index.
   */
  public boolean isAtLevel(Level level) {
    return this.level == level
        || this.level != null && level != null
           && this.level.getElevation() == level.getElevation()
           && this.level.getElevationIndex() < level.getElevationIndex();

  }

  /**
   * Returns an approximate length of this polyline.
   */
  public float getLength() {
    float [] firstPoint = new float [2];
    float [] previousPoint = new float [2];
    float [] point = new float [2];
    float length = 0;
    for (PathIterator it = getPolylinePath().getPathIterator(null, 0.1); !it.isDone(); it.next()) {
      switch (it.currentSegment(point)) {
        case PathIterator.SEG_CLOSE :
          length += Point2D.distance(firstPoint [0], firstPoint [1], previousPoint [0], previousPoint [1]);
          break;
        case PathIterator.SEG_MOVETO :
          System.arraycopy(point, 0, firstPoint, 0, 2);
          System.arraycopy(point, 0, previousPoint, 0, 2);
          break;
        case PathIterator.SEG_LINETO :
          length += Point2D.distance(previousPoint [0], previousPoint [1], point [0], point [1]);
          System.arraycopy(point, 0, previousPoint, 0, 2);
          break;
      }
    }
    return length;
  }

  /**
   * Returns <code>true</code> if this polyline intersects
   * with the horizontal rectangle which opposite corners are at points
   * (<code>x0</code>, <code>y0</code>) and (<code>x1</code>, <code>y1</code>).
   */
  public boolean intersectsRectangle(float x0, float y0, float x1, float y1) {
    Rectangle2D rectangle = new Rectangle2D.Float(x0, y0, 0, 0);
    rectangle.add(x1, y1);
    return getShape().intersects(rectangle);
  }

  /**
   * Returns <code>true</code> if this polyline contains
   * the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
   */
  public boolean containsPoint(float x, float y, float margin) {
    return containsShapeAtWithMargin(getShape(), x, y, margin);
  }

  /**
   * Returns the index of the point of this polyline equal to
   * the point at (<code>x</code>, <code>y</code>) with a given <code>margin</code>.
   * @return the index of the first found point or -1.
   */
  public int getPointIndexAt(float x, float y, float margin) {
    for (int i = 0; i < this.points.length; i++) {
      if (Math.abs(x - this.points [i][0]) <= margin && Math.abs(y - this.points [i][1]) <= margin) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Returns <code>true</code> if <code>shape</code> contains
   * the point at (<code>x</code>, <code>y</code>)
   * with a given <code>margin</code>.
   */
  private boolean containsShapeAtWithMargin(Shape shape, float x, float y, float margin) {
    if (margin == 0) {
      return shape.contains(x, y);
    } else {
      return shape.intersects(x - margin, y - margin, 2 * margin, 2 * margin);
    }
  }

  /**
   * Returns the path matching this polyline.
   */
  private Shape getPolylinePath() {
    if (this.polylinePathCache == null) {
      GeneralPath polylinePath = new GeneralPath();
      if (this.joinStyle == JoinStyle.CURVED) {
        for (int i = 0, n = this.closedPath ? this.points.length : this.points.length - 1; i < n; i++) {
          CubicCurve2D.Float curve2D = new CubicCurve2D.Float();
          float [] previousPoint = this.points [i == 0 ?  this.points.length - 1  : i - 1];
          float [] point         = this.points [i];
          float [] nextPoint     = this.points [i == this.points.length - 1 ?  0  : i + 1];
          float [] vectorToBisectorPoint = new float [] {nextPoint [0] - previousPoint [0], nextPoint [1] - previousPoint [1]};
          float [] nextNextPoint     = this.points [(i + 2) % this.points.length];
          float [] vectorToBisectorNextPoint = new float [] {point[0] - nextNextPoint [0], point[1] - nextNextPoint [1]};
          curve2D.setCurve(point[0], point[1],
              point [0] + (i != 0 || this.closedPath  ? vectorToBisectorPoint [0] / 3.625f  : 0),
              point [1] + (i != 0 || this.closedPath  ? vectorToBisectorPoint [1] / 3.625f  : 0),
              nextPoint [0] + (i != this.points.length - 2 || this.closedPath  ? vectorToBisectorNextPoint [0] / 3.625f  : 0),
              nextPoint [1] + (i != this.points.length - 2 || this.closedPath  ? vectorToBisectorNextPoint [1] / 3.625f  : 0),
              nextPoint [0], nextPoint [1]);
          polylinePath.append(curve2D, true);
        }
      } else {
        polylinePath.moveTo(this.points [0][0], this.points [0][1]);
        for (int i = 1; i < this.points.length; i++) {
          polylinePath.lineTo(this.points [i][0], this.points [i][1]);
        }
        if (this.closedPath) {
          polylinePath.closePath();
        }
      }
      // Cache polylineShape
      this.polylinePathCache = polylinePath;
    }
    return this.polylinePathCache;
  }

  /**
   * Returns the shape matching this polyline.
   */
  private Shape getShape() {
    if (this.shapeCache == null) {
      this.shapeCache = new BasicStroke(this.thickness).createStrokedShape(getPolylinePath());
    }
    return this.shapeCache;
  }

  /**
   * Moves this polyline of (<code>dx</code>, <code>dy</code>) units.
   */
  public void move(float dx, float dy) {
    if (dx != 0 || dy != 0) {
      float [][] points = getPoints();
      for (int i = 0; i < points.length; i++) {
        points [i][0] += dx;
        points [i][1] += dy;
      }
      updatePoints(points);
    }
  }

  /**
   * Returns a clone of this polyline.
   */
  @Override
  public Polyline clone() {
    Polyline clone = (Polyline)super.clone();
    clone.level = null;
    return clone;
  }
}
