All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.eteks.sweethome3d.model.Wall Maven / Gradle / Ivy

/*
 * Wall.java 3 juin 2006
 *
 * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks 
 *
 * 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.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * A wall of a home plan.
 * @author Emmanuel Puybaret
 */
public class Wall extends HomeObject implements Selectable, Elevatable {
  /**
   * The properties of a wall that may change. PropertyChangeListeners added 
   * to a wall will be notified under a property name equal to the string value of one these properties.
   */
  public enum Property {X_START, Y_START, X_END, Y_END, ARC_EXTENT, WALL_AT_START, WALL_AT_END, 
                        THICKNESS, HEIGHT, HEIGHT_AT_END, 
                        LEFT_SIDE_COLOR, LEFT_SIDE_TEXTURE, LEFT_SIDE_SHININESS, LEFT_SIDE_BASEBOARD, 
                        RIGHT_SIDE_COLOR, RIGHT_SIDE_TEXTURE, RIGHT_SIDE_SHININESS, RIGHT_SIDE_BASEBOARD,
                        PATTERN, TOP_COLOR, LEVEL}
  
  private static final long serialVersionUID = 1L;
  
  private float               xStart;
  private float               yStart;
  private float               xEnd;
  private float               yEnd; 
  private Float               arcExtent; 
  private Wall                wallAtStart;
  private Wall                wallAtEnd;
  private float               thickness;
  private Float               height;
  private Float               heightAtEnd;
  private Integer             leftSideColor;
  private HomeTexture         leftSideTexture;
  private float               leftSideShininess;
  private Baseboard           leftSideBaseboard;
  private Integer             rightSideColor;
  private HomeTexture         rightSideTexture;
  private float               rightSideShininess;
  private Baseboard           rightSideBaseboard;
  private boolean             symmetric = true;
  private TextureImage        pattern;  
  private Integer             topColor;
  private Level               level;
  
  private transient PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
  private transient float [][] pointsCache;
  private transient float [][] pointsIncludingBaseboardsCache;


  /**
   * Creates a wall from (xStart,yStart)
   * to (xEnd, yEnd), 
   * with given thickness. Height, left and right colors are null.
   * @deprecated specify a height with the {@linkplain #Wall(float, float, float, float, float, float) other constructor}.
   */
  public Wall(float xStart, float yStart, float xEnd, float yEnd, float thickness) {
    this(xStart, yStart, xEnd, yEnd, thickness, 0);
  }
  
  /**
   * Creates a wall from (xStart,yStart)
   * to (xEnd, yEnd), 
   * with given thickness and height. Pattern, left and right colors are null.
   */
  public Wall(float xStart, float yStart, float xEnd, float yEnd, float thickness, float height) {
    this(xStart, yStart, xEnd, yEnd, thickness, height, null);
  }
  
  /**
   * Creates a wall from (xStart,yStart)
   * to (xEnd, yEnd), 
   * with given thickness, height and pattern. 
   * Colors are null.
   * @since 4.0
   */
  public Wall(float xStart, float yStart, float xEnd, float yEnd, float thickness, float height, TextureImage pattern) {
    this.xStart = xStart;
    this.yStart = yStart;
    this.xEnd = xEnd;
    this.yEnd = yEnd;
    this.thickness = thickness;
    this.height = height;
    this.pattern = pattern;
  }
  
  /**
   * Initializes new wall transient fields  
   * and reads wall from in stream with default reading method.
   */
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    this.propertyChangeSupport = new PropertyChangeSupport(this);
    in.defaultReadObject();
  }

  /**
   * Adds the property change listener in parameter to this wall.
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    this.propertyChangeSupport.addPropertyChangeListener(listener);
  }

  /**
   * Removes the property change listener in parameter from this wall.
   */
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    this.propertyChangeSupport.removePropertyChangeListener(listener);
  }

  /**
   * Returns the start point abscissa of this wall.
   */
  public float getXStart() {
    return this.xStart;
  }

  /**
   * Sets the start point abscissa of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setXStart(float xStart) {
    if (xStart != this.xStart) {
      float oldXStart = this.xStart;
      this.xStart = xStart;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.X_START.name(), oldXStart, xStart);
    }
  }

  /**
   * Returns the start point ordinate of this wall.
   */
  public float getYStart() {
    return this.yStart;
  }

  /**
   * Sets the start point ordinate of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setYStart(float yStart) {
    if (yStart != this.yStart) {
      float oldYStart = this.yStart;
      this.yStart = yStart;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.Y_START.name(), oldYStart, yStart);
    }
  }

  /**
   * Returns the end point abscissa of this wall.
   */
  public float getXEnd() {
    return this.xEnd;
  }

  /**
   * Sets the end point abscissa of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setXEnd(float xEnd) {
    if (xEnd != this.xEnd) {
      float oldXEnd = this.xEnd;
      this.xEnd = xEnd;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.X_END.name(), oldXEnd, xEnd);
    }
  }

  /**
   * Returns the end point ordinate of this wall.
   */
  public float getYEnd() {
    return this.yEnd;
  }

  /**
   * Sets the end point ordinate of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setYEnd(float yEnd) {
    if (yEnd != this.yEnd) {
      float oldYEnd = this.yEnd;
      this.yEnd = yEnd;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.Y_END.name(), oldYEnd, yEnd);
    }
  }

  /**
   * Returns the length of this wall.
   * @since 2.0
   */
  public float getLength() {
    if (this.arcExtent == null
        || this.arcExtent.floatValue() == 0) {
      return (float)Point2D.distance(this.xStart, this.yStart, this.xEnd, this.yEnd);
    } else {
      float [] arcCircleCenter = getArcCircleCenter();
      float arcCircleRadius = (float)Point2D.distance(this.xStart, this.yStart, 
          arcCircleCenter [0], arcCircleCenter [1]);
      return Math.abs(this.arcExtent) * arcCircleRadius;
    }
  }
  
  /**
   * Returns the distance from the start point of this wall to its end point.
   * @since 3.0
   */
  public float getStartPointToEndPointDistance() {
    return (float)Point2D.distance(this.xStart, this.yStart, this.xEnd, this.yEnd);
  }
  
  /**
   * Sets the arc extent of a round wall.
   * @since 3.0
   */
  public void setArcExtent(Float arcExtent) {
    if (arcExtent != this.arcExtent
        || (arcExtent != null && !arcExtent.equals(this.arcExtent))) {
      Float oldArcExtent = this.arcExtent;
      this.arcExtent = arcExtent;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.ARC_EXTENT.name(), 
          oldArcExtent, arcExtent);
    }
  }

  /**
   * Returns the arc extent of a round wall or null if this wall isn't round.
   * @since 3.0
   */
  public Float getArcExtent() {
    return this.arcExtent;
  }

  /**
   * Returns the abscissa of the arc circle center of this wall.
   * If the wall isn't round, the return abscissa is at the middle of the wall. 
   * @since 3.0
   */
  public float getXArcCircleCenter() {
    if (this.arcExtent == null) {
      return (this.xStart + this.xEnd) / 2; 
    } else {
      return getArcCircleCenter() [0];
    }
  }

  /**
   * Returns the ordinate of the arc circle center of this wall.
   * If the wall isn't round, the return ordinate is at the middle of the wall. 
   * @since 3.0
   */
  public float getYArcCircleCenter() {
    if (this.arcExtent == null) {
      return (this.yStart + this.yEnd) / 2;
    } else {
      return getArcCircleCenter() [1];
    }
  }

  /**
   * Returns the coordinates of the arc circle center of this wall.
   */
  private float [] getArcCircleCenter() {
    double startToEndPointsDistance = Point2D.distance(this.xStart, this.yStart, this.xEnd, this.yEnd);
    double wallToStartPointArcCircleCenterAngle = Math.abs(this.arcExtent) > Math.PI 
        ? -(Math.PI + this.arcExtent) / 2
        : (Math.PI - this.arcExtent) / 2;
    float arcCircleCenterToWallDistance = -(float)(Math.tan(wallToStartPointArcCircleCenterAngle) 
        * startToEndPointsDistance / 2); 
    float xMiddlePoint = (this.xStart + this.xEnd) / 2;
    float yMiddlePoint = (this.yStart + this.yEnd) / 2;
    double angle = Math.atan2(this.xStart - this.xEnd, this.yEnd - this.yStart);
    return new float [] {(float)(xMiddlePoint + arcCircleCenterToWallDistance * Math.cos(angle)), 
                         (float)(yMiddlePoint + arcCircleCenterToWallDistance * Math.sin(angle))};
  }
  
  /**
   * Returns the wall joined to this wall at start point.
   */
  public Wall getWallAtStart() {
    return this.wallAtStart;
  }

  /**
   * Sets the wall joined to this wall at start point. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   * If the start point of this wall is attached to an other wall, it will be detached 
   * from this wall, and wall listeners will receive a change notification.
   * @param wallAtStart a wall or null to detach this wall
   *          from any wall it was attached to before.
   */
  public void setWallAtStart(Wall wallAtStart) {
    setWallAtStart(wallAtStart, true);
  }

  /**
   * Sets the wall joined to this wall at start point and detachs the wall at start
   * from this wall if detachJoinedWallAtStart is true. 
   */
  private void setWallAtStart(Wall wallAtStart, boolean detachJoinedWallAtStart) {
    if (wallAtStart != this.wallAtStart) {
      Wall oldWallAtStart = this.wallAtStart;
      this.wallAtStart = wallAtStart;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.WALL_AT_START.name(), 
          oldWallAtStart, wallAtStart);
      
      if (detachJoinedWallAtStart) {
        detachJoinedWall(oldWallAtStart);
      }
    }
  }

  /**
   * Returns the wall joined to this wall at end point.
   */
  public Wall getWallAtEnd() {
    return this.wallAtEnd;
  }
 
 
  /**
   * Sets the wall joined to this wall at end point. Once this wall is updated, 
   * listeners added to this wall will receive a change notification. 
   * If the end point of this wall is attached to an other wall, it will be detached 
   * from this wall, and wall listeners will receive a change notification.
   * @param wallAtEnd a wall or null to detach this wall
   *          from any wall it was attached to before.
   */
  public void setWallAtEnd(Wall wallAtEnd) {
    setWallAtEnd(wallAtEnd, true);
  }

  /**
   * Sets the wall joined to this wall at end point and detachs the wall at end
   * from this wall if detachJoinedWallAtEnd is true. 
   */
  private void setWallAtEnd(Wall wallAtEnd, boolean detachJoinedWallAtEnd) {
    if (wallAtEnd != this.wallAtEnd) {
      Wall oldWallAtEnd = this.wallAtEnd;
      this.wallAtEnd = wallAtEnd;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.WALL_AT_END.name(), 
          oldWallAtEnd, wallAtEnd);
      
      if (detachJoinedWallAtEnd) {
        detachJoinedWall(oldWallAtEnd);
      }
    }
  }

  /**
   * Detaches joinedWall from this wall.
   */
  private void detachJoinedWall(Wall joinedWall) {
    // Detach the previously attached wall 
    if (joinedWall != null) {
      if (joinedWall.getWallAtStart() == this) {
        joinedWall.setWallAtStart(null, false);
      } else if (joinedWall.getWallAtEnd() == this) {
        joinedWall.setWallAtEnd(null, false);
      } 
    }
  }

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

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

  /**
   * Returns the height of this wall. If {@link #getHeightAtEnd() getHeightAtEnd}
   * returns a value not null, the returned height should be
   * considered as the height of this wall at its start point.
   */
  public Float getHeight() {
    return this.height;
  }

  /**
   * Sets the height of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setHeight(Float height) {
    if (height != this.height
        || (height != null && !height.equals(this.height))) {
      Float oldHeight = this.height;
      this.height = height;
      this.propertyChangeSupport.firePropertyChange(Property.HEIGHT.name(), 
          oldHeight, height);
    }
  }

  /**
   * Returns the height of this wall at its end point.
   */
  public Float getHeightAtEnd() {
    return this.heightAtEnd;
  }

  /**
   * Sets the height of this wall at its end point. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setHeightAtEnd(Float heightAtEnd) {
    if (heightAtEnd != this.heightAtEnd
        && (heightAtEnd == null || !heightAtEnd.equals(this.heightAtEnd))) {
      Float oldHeightAtEnd = this.heightAtEnd;
      this.heightAtEnd = heightAtEnd;
      this.propertyChangeSupport.firePropertyChange(Property.HEIGHT_AT_END.name(), 
          oldHeightAtEnd, heightAtEnd);
    }
  }

  /**
   * Returns true if the height of this wall is different
   * at its start and end points. 
   */
  public boolean isTrapezoidal() {
    return this.height != null
        && this.heightAtEnd != null
        && !this.height.equals(this.heightAtEnd);  
  }
  
  /**
   * Returns left side color of this wall. This is the color of the left side 
   * of this wall when you go through wall from start point to end point.
   */
  public Integer getLeftSideColor() {
    return this.leftSideColor;
  }

  /**
   * Sets left side color of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setLeftSideColor(Integer leftSideColor) {
    if (leftSideColor != this.leftSideColor
        && (leftSideColor == null || !leftSideColor.equals(this.leftSideColor))) {
      Integer oldLeftSideColor = this.leftSideColor;
      this.leftSideColor = leftSideColor;
      this.propertyChangeSupport.firePropertyChange(Property.LEFT_SIDE_COLOR.name(), 
          oldLeftSideColor, leftSideColor);
    }
  }

  /**
   * Returns right side color of this wall. This is the color of the right side 
   * of this wall when you go through wall from start point to end point.
   */
  public Integer getRightSideColor() {
    return this.rightSideColor;
  }

  /**
   * Sets right side color of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setRightSideColor(Integer rightSideColor) {
    if (rightSideColor != this.rightSideColor
        && (rightSideColor == null || !rightSideColor.equals(this.rightSideColor))) {
      Integer oldLeftSideColor = this.rightSideColor;
      this.rightSideColor = rightSideColor;
      this.propertyChangeSupport.firePropertyChange(Property.RIGHT_SIDE_COLOR.name(), 
          oldLeftSideColor, rightSideColor);
    }
  }

  
  /**
   * Returns the left side texture of this wall.
   */
  public HomeTexture getLeftSideTexture() {
    return this.leftSideTexture;
  }

  /**
   * Sets the left side texture of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setLeftSideTexture(HomeTexture leftSideTexture) {
    if (leftSideTexture != this.leftSideTexture
        && (leftSideTexture == null || !leftSideTexture.equals(this.leftSideTexture))) {
      HomeTexture oldLeftSideTexture = this.leftSideTexture;
      this.leftSideTexture = leftSideTexture;
      this.propertyChangeSupport.firePropertyChange(Property.LEFT_SIDE_TEXTURE.name(), 
          oldLeftSideTexture, leftSideTexture);
    }
  }

  /**
   * Returns the right side texture of this wall.
   */
  public HomeTexture getRightSideTexture() {
    return this.rightSideTexture;
  }

  /**
   * Sets the right side texture of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   */
  public void setRightSideTexture(HomeTexture rightSideTexture) {
    if (rightSideTexture != this.rightSideTexture
        && (rightSideTexture == null || !rightSideTexture.equals(this.rightSideTexture))) {
      HomeTexture oldLeftSideTexture = this.rightSideTexture;
      this.rightSideTexture = rightSideTexture;
      this.propertyChangeSupport.firePropertyChange(Property.RIGHT_SIDE_TEXTURE.name(), 
          oldLeftSideTexture, rightSideTexture);
    }
  }

  /**
   * Returns the left side shininess of this wall.
   * @return a value between 0 (matt) and 1 (very shiny)  
   * @since 3.0
   */
  public float getLeftSideShininess() {
    return this.leftSideShininess;
  }

  /**
   * Sets the left side shininess of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   * @since 3.0
   */
  public void setLeftSideShininess(float leftSideShininess) {
    if (leftSideShininess != this.leftSideShininess) {
      float oldLeftSideShininess = this.leftSideShininess;
      this.leftSideShininess = leftSideShininess;
      this.propertyChangeSupport.firePropertyChange(Property.LEFT_SIDE_SHININESS.name(), oldLeftSideShininess, leftSideShininess);
    }
  }

  /**
   * Returns the right side shininess of this wall.
   * @return a value between 0 (matt) and 1 (very shiny)  
   * @since 3.0
   */
  public float getRightSideShininess() {
    return this.rightSideShininess;
  }

  /**
   * Sets the right side shininess of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   * @since 3.0
   */
  public void setRightSideShininess(float rightSideShininess) {
    if (rightSideShininess != this.rightSideShininess) {
      float oldRightSideShininess = this.rightSideShininess;
      this.rightSideShininess = rightSideShininess;
      this.propertyChangeSupport.firePropertyChange(Property.RIGHT_SIDE_SHININESS.name(), oldRightSideShininess, rightSideShininess);
    }
  }

  /**
   * Returns the left side baseboard of this wall.
   * @since 5.0
   */
  public Baseboard getLeftSideBaseboard() {
    return this.leftSideBaseboard;
  }

  /**
   * Sets the left side baseboard of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   * @since 5.0
   */
  public void setLeftSideBaseboard(Baseboard leftSideBaseboard) {
    if (leftSideBaseboard != this.leftSideBaseboard
        && (leftSideBaseboard == null || !leftSideBaseboard.equals(this.leftSideBaseboard))) {
      Baseboard oldLeftSideBaseboard = this.leftSideBaseboard;
      this.leftSideBaseboard = leftSideBaseboard;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.LEFT_SIDE_BASEBOARD.name(), oldLeftSideBaseboard, leftSideBaseboard);
    }
  }

  /**
   * Returns the right side baseboard of this wall.
   * @since 5.0
   */
  public Baseboard getRightSideBaseboard() {
    return this.rightSideBaseboard;
  }

  /**
   * Sets the right side baseboard of this wall. Once this wall is updated, 
   * listeners added to this wall will receive a change notification.
   * @since 5.0
   */
  public void setRightSideBaseboard(Baseboard rightSideBaseboard) {
    if (rightSideBaseboard != this.rightSideBaseboard
        && (rightSideBaseboard == null || !rightSideBaseboard.equals(this.rightSideBaseboard))) {
      Baseboard oldRightSideBaseboard = this.rightSideBaseboard;
      this.rightSideBaseboard = rightSideBaseboard;
      clearPointsCache();
      this.propertyChangeSupport.firePropertyChange(Property.RIGHT_SIDE_BASEBOARD.name(), oldRightSideBaseboard, rightSideBaseboard);
    }
  }

  /**
   * Returns the pattern of this wall in the plan.
   * @since 3.3
   */
  public TextureImage getPattern() {
    return this.pattern;
  }
  
  /**
   * Sets the pattern of this wall in the plan, and notifies
   * listeners of this change.
   * @since 3.3 
   */
  public void setPattern(TextureImage pattern) {
    if (this.pattern != pattern) {
      TextureImage oldPattern = this.pattern;
      this.pattern = pattern;
      this.propertyChangeSupport.firePropertyChange(Property.PATTERN.name(), 
          oldPattern, pattern);
    }
  }

  /**
   * Returns the color of the top of this wall in the 3D view.
   * @since 4.0
   */
  public Integer getTopColor() {
    return this.topColor;
  }
  
  /**
   * Sets the color of the top of this wall in the 3D view, and notifies
   * listeners of this change.
   * @since 4.0 
   */
  public void setTopColor(Integer topColor) {
    if (this.topColor != topColor
        && (topColor == null || !topColor.equals(this.topColor))) {
      Integer oldTopColor = this.topColor;
      this.topColor = topColor;
      this.propertyChangeSupport.firePropertyChange(Property.TOP_COLOR.name(), 
          oldTopColor, topColor);
    }
  }

  /**
   * Returns the level which this wall belongs to. 
   * @since 3.4
   */
  public Level getLevel() {
    return this.level;
  }

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

  /**
   * Returns true if this wall is at the given level 
   * or at a level with the same elevation and a smaller elevation index
   * or if the elevation of its highest point is higher than level elevation.
   * @since 3.4
   */
  public boolean isAtLevel(Level level) {
    if (this.level == level) {
      return true;
    } else if (this.level != null && level != null) {
      float wallLevelElevation = this.level.getElevation();
      float levelElevation = level.getElevation();
      return wallLevelElevation == levelElevation
             && this.level.getElevationIndex() < level.getElevationIndex()
          || wallLevelElevation < levelElevation
             && wallLevelElevation + getWallMaximumHeight() > levelElevation;
    } else {
      return false;
    }
  }
  
  /**
   * Returns the maximum height of the given wall.
   */
  private float getWallMaximumHeight() {
    if (this.height == null) {
      // Shouldn't happen
      return 0; 
    } else if (isTrapezoidal()) {
      return Math.max(this.height, this.heightAtEnd);
    } else {
      return this.height;
    }
  }

  /**
   * Clears the points cache of this wall and of the walls attached to it.
   */
  private void clearPointsCache() {
    this.pointsCache = null;
    this.pointsIncludingBaseboardsCache = null;
    if (this.wallAtStart != null ) {
      this.wallAtStart.pointsCache = null;
      this.wallAtStart.pointsIncludingBaseboardsCache = null;
    }
    if (this.wallAtEnd != null) {
      this.wallAtEnd.pointsCache = null;
      this.wallAtEnd.pointsIncludingBaseboardsCache = null;
    }
  }
  
  /**
   * Returns the points of each corner of a wall not including its baseboards. 
   * @return an array of the (x,y) coordinates of the wall corners.
   *    For a straight wall, the points at index 0 and 3 indicates the start of the wall, 
   *    while the points at index 1 and 2 indicates the end of the wall. 
   */
  public float [][] getPoints() {
    return getPoints(false);
  }

  /**
   * Returns the points of each corner of a wall possibly including its baseboards. 
   * @since 5.0
   */
  public float [][] getPoints(boolean includeBaseboards) {
    if (includeBaseboards
        && (this.leftSideBaseboard != null
            || this.rightSideBaseboard != null)) {
      if (this.pointsIncludingBaseboardsCache == null) {
        this.pointsIncludingBaseboardsCache = getShapePoints(true);
      }
      return clonePoints(this.pointsIncludingBaseboardsCache);
    } else {
      if (this.pointsCache == null) {
        this.pointsCache = getShapePoints(false);
      }
      return clonePoints(this.pointsCache);
    }
  }

  /**
   * Return a clone of the given points array.
   */
  private float [][] clonePoints(float [][] points) {
    float [][] clonedPoints = new float [points.length][];
    for (int i = 0; i < points.length; i++) {
      clonedPoints [i] = points [i].clone();
    }
    return clonedPoints;
  }

  /**
   * Returns the points of the wall possibly including baseboards thickness.
   */
  private float [][] getShapePoints(boolean includeBaseboards) {
    final float epsilon = 0.01f;
    float [][] wallPoints = getUnjoinedShapePoints(includeBaseboards);
    int leftSideStartPointIndex = 0;
    int rightSideStartPointIndex = wallPoints.length - 1;
    int leftSideEndPointIndex = wallPoints.length / 2 - 1;
    int rightSideEndPointIndex = wallPoints.length / 2;
    float limit = 2 * this.thickness;
    // If wall is joined to a wall at its start, 
    // compute the intersection between their outlines 
    if (this.wallAtStart != null) {
      float [][] wallAtStartPoints = this.wallAtStart.getUnjoinedShapePoints(includeBaseboards);
      int wallAtStartLeftSideStartPointIndex = 0;
      int wallAtStartRightSideStartPointIndex = wallAtStartPoints.length - 1;
      int wallAtStartLeftSideEndPointIndex = wallAtStartPoints.length / 2 - 1;
      int wallAtStartRightSideEndPointIndex = wallAtStartPoints.length / 2;
      boolean wallAtStartJoinedAtEnd = this.wallAtStart.getWallAtEnd() == this
          // Check the coordinates when walls are joined to each other at both ends 
          && (this.wallAtStart.getWallAtStart() != this
              || (this.wallAtStart.xEnd == this.xStart
                  && this.wallAtStart.yEnd == this.yStart));
      boolean wallAtStartJoinedAtStart = this.wallAtStart.getWallAtStart() == this
          // Check the coordinates when walls are joined to each other at both ends 
          && (this.wallAtStart.getWallAtEnd() != this
              || (this.wallAtStart.xStart == this.xStart
                  && this.wallAtStart.yStart == this.yStart));
      float [][] wallAtStartPointsCache = includeBaseboards 
          ? this.wallAtStart.pointsIncludingBaseboardsCache
          : this.wallAtStart.pointsCache;
      if (wallAtStartJoinedAtEnd) {
        computeIntersection(wallPoints [leftSideStartPointIndex], wallPoints [leftSideStartPointIndex + 1], 
            wallAtStartPoints [wallAtStartLeftSideEndPointIndex], wallAtStartPoints [wallAtStartLeftSideEndPointIndex - 1], limit);
        computeIntersection(wallPoints [rightSideStartPointIndex], wallPoints [rightSideStartPointIndex - 1],  
            wallAtStartPoints [wallAtStartRightSideEndPointIndex], wallAtStartPoints [wallAtStartRightSideEndPointIndex + 1], limit);

        // If the computed start point of this wall and the computed end point of the wall at start 
        // are equal to within epsilon, share the exact same point to avoid computing errors on areas
        if (wallAtStartPointsCache != null) {
          if (Math.abs(wallPoints [leftSideStartPointIndex][0] - wallAtStartPointsCache [wallAtStartLeftSideEndPointIndex][0]) < epsilon
              && Math.abs(wallPoints [leftSideStartPointIndex][1] - wallAtStartPointsCache [wallAtStartLeftSideEndPointIndex][1]) < epsilon) {
            wallPoints [leftSideStartPointIndex] = wallAtStartPointsCache [wallAtStartLeftSideEndPointIndex];
          }                        
          if (Math.abs(wallPoints [rightSideStartPointIndex][0] - wallAtStartPointsCache [wallAtStartRightSideEndPointIndex][0]) < epsilon
              && Math.abs(wallPoints [rightSideStartPointIndex][1] - wallAtStartPointsCache [wallAtStartRightSideEndPointIndex][1]) < epsilon) {
            wallPoints [rightSideStartPointIndex] = wallAtStartPointsCache [wallAtStartRightSideEndPointIndex];
          }
        }
      } else if (wallAtStartJoinedAtStart) {
        computeIntersection(wallPoints [leftSideStartPointIndex], wallPoints [leftSideStartPointIndex + 1], 
            wallAtStartPoints [wallAtStartRightSideStartPointIndex], wallAtStartPoints [wallAtStartRightSideStartPointIndex - 1], limit);
        computeIntersection(wallPoints [rightSideStartPointIndex], wallPoints [rightSideStartPointIndex - 1],  
            wallAtStartPoints [wallAtStartLeftSideStartPointIndex], wallAtStartPoints [wallAtStartLeftSideStartPointIndex + 1], limit);
        
        // If the computed start point of this wall and the computed start point of the wall at start 
        // are equal to within epsilon, share the exact same point to avoid computing errors on areas 
        if (wallAtStartPointsCache != null) {
          if (Math.abs(wallPoints [leftSideStartPointIndex][0] - wallAtStartPointsCache [wallAtStartRightSideStartPointIndex][0]) < epsilon
              && Math.abs(wallPoints [leftSideStartPointIndex][1] - wallAtStartPointsCache [wallAtStartRightSideStartPointIndex][1]) < epsilon) {
            wallPoints [leftSideStartPointIndex] = wallAtStartPointsCache [wallAtStartRightSideStartPointIndex];
          }                            
          if (wallAtStartPointsCache != null
              && Math.abs(wallPoints [rightSideStartPointIndex][0] - wallAtStartPointsCache [wallAtStartLeftSideStartPointIndex][0]) < epsilon
              && Math.abs(wallPoints [rightSideStartPointIndex][1] - wallAtStartPointsCache [wallAtStartLeftSideStartPointIndex][1]) < epsilon) {
            wallPoints [rightSideStartPointIndex] = wallAtStartPointsCache [wallAtStartLeftSideStartPointIndex];
          }
        }
      }
    }
  
    // If wall is joined to a wall at its end, 
    // compute the intersection between their outlines 
    if (this.wallAtEnd != null) {
      float [][] wallAtEndPoints = this.wallAtEnd.getUnjoinedShapePoints(includeBaseboards);
      int wallAtEndLeftSideStartPointIndex = 0;
      int wallAtEndRightSideStartPointIndex = wallAtEndPoints.length - 1;
      int wallAtEndLeftSideEndPointIndex = wallAtEndPoints.length / 2 - 1;
      int wallAtEndRightSideEndPointIndex = wallAtEndPoints.length / 2;
      boolean wallAtEndJoinedAtStart = this.wallAtEnd.getWallAtStart() == this
          // Check the coordinates when walls are joined to each other at both ends 
          && (this.wallAtEnd.getWallAtEnd() != this
              || (this.wallAtEnd.xStart == this.xEnd
                  && this.wallAtEnd.yStart == this.yEnd));
      boolean wallAtEndJoinedAtEnd = this.wallAtEnd.getWallAtEnd() == this
          // Check the coordinates when walls are joined to each other at both ends 
          && (this.wallAtEnd.getWallAtStart() != this
              || (this.wallAtEnd.xEnd == this.xEnd
                  && this.wallAtEnd.yEnd == this.yEnd));
      float [][] wallAtEndPointsCache = includeBaseboards 
          ? this.wallAtEnd.pointsIncludingBaseboardsCache
          : this.wallAtEnd.pointsCache;
      if (wallAtEndJoinedAtStart) {
        computeIntersection(wallPoints [leftSideEndPointIndex], wallPoints [leftSideEndPointIndex - 1], 
            wallAtEndPoints [wallAtEndLeftSideStartPointIndex], wallAtEndPoints [wallAtEndLeftSideStartPointIndex + 1], limit);
        computeIntersection(wallPoints [rightSideEndPointIndex], wallPoints [rightSideEndPointIndex + 1], 
            wallAtEndPoints [wallAtEndRightSideStartPointIndex], wallAtEndPoints [wallAtEndRightSideStartPointIndex - 1], limit);

        // If the computed end point of this wall and the computed start point of the wall at end 
        // are equal to within epsilon, share the exact same point to avoid computing errors on areas 
        if (wallAtEndPointsCache != null) {
          if (Math.abs(wallPoints [leftSideEndPointIndex][0] - wallAtEndPointsCache [wallAtEndLeftSideStartPointIndex][0]) < epsilon
              && Math.abs(wallPoints [leftSideEndPointIndex][1] - wallAtEndPointsCache [wallAtEndLeftSideStartPointIndex][1]) < epsilon) {
            wallPoints [leftSideEndPointIndex] = wallAtEndPointsCache [wallAtEndLeftSideStartPointIndex];
          }                        
          if (Math.abs(wallPoints [rightSideEndPointIndex][0] - wallAtEndPointsCache [wallAtEndRightSideStartPointIndex][0]) < epsilon
              && Math.abs(wallPoints [rightSideEndPointIndex][1] - wallAtEndPointsCache [wallAtEndRightSideStartPointIndex][1]) < epsilon) {
            wallPoints [rightSideEndPointIndex] = wallAtEndPointsCache [wallAtEndRightSideStartPointIndex];
          }
        }
      } else if (wallAtEndJoinedAtEnd) {
        computeIntersection(wallPoints [leftSideEndPointIndex], wallPoints [leftSideEndPointIndex - 1],  
            wallAtEndPoints [wallAtEndRightSideEndPointIndex], wallAtEndPoints [wallAtEndRightSideEndPointIndex + 1], limit);
        computeIntersection(wallPoints [rightSideEndPointIndex], wallPoints [rightSideEndPointIndex + 1], 
            wallAtEndPoints [wallAtEndLeftSideEndPointIndex], wallAtEndPoints [wallAtEndLeftSideEndPointIndex - 1], limit);

        // If the computed end point of this wall and the computed start point of the wall at end 
        // are equal to within epsilon, share the exact same point to avoid computing errors on areas 
        if (wallAtEndPointsCache != null) {
          if (Math.abs(wallPoints [leftSideEndPointIndex][0] - wallAtEndPointsCache [wallAtEndRightSideEndPointIndex][0]) < epsilon
              && Math.abs(wallPoints [leftSideEndPointIndex][1] - wallAtEndPointsCache [wallAtEndRightSideEndPointIndex][1]) < epsilon) {
            wallPoints [leftSideEndPointIndex] = wallAtEndPointsCache [wallAtEndRightSideEndPointIndex];
          }                        
          if (Math.abs(wallPoints [rightSideEndPointIndex][0] - wallAtEndPointsCache [wallAtEndLeftSideEndPointIndex][0]) < epsilon
              && Math.abs(wallPoints [rightSideEndPointIndex][1] - wallAtEndPointsCache [wallAtEndLeftSideEndPointIndex][1]) < epsilon) {
            wallPoints [rightSideEndPointIndex] = wallAtEndPointsCache [wallAtEndLeftSideEndPointIndex];
          }
        }
      }
    }
    return wallPoints;
  }

  /**
   * Computes the rectangle or the circle arc of a wall according to its thickness 
   * and possibly the thickness of its baseboards. 
   */  
  private float [][] getUnjoinedShapePoints(boolean includeBaseboards) {
    if (this.arcExtent != null
        && this.arcExtent.floatValue() != 0
        && Point2D.distanceSq(this.xStart, this.yStart, this.xEnd, this.yEnd) > 1E-10) {
      float [] arcCircleCenter = getArcCircleCenter();
      float startAngle = (float)Math.atan2(arcCircleCenter [1] - this.yStart, arcCircleCenter [0] - this.xStart);
      startAngle += 2 * (float)Math.atan2(this.yStart - this.yEnd, this.xEnd - this.xStart);
      float arcCircleRadius = (float)Point2D.distance(arcCircleCenter [0], arcCircleCenter [1], this.xStart, this.yStart);
      float exteriorArcRadius = arcCircleRadius + this.thickness / 2;
      float interiorArcRadius = Math.max(0, arcCircleRadius - this.thickness / 2);
      float exteriorArcLength = exteriorArcRadius * Math.abs(this.arcExtent);
      float angleDelta = this.arcExtent / (float)Math.sqrt(exteriorArcLength);
      int angleStepCount = (int)(this.arcExtent / angleDelta);
      if (includeBaseboards) {
        if (angleDelta > 0) {
          if (this.leftSideBaseboard != null) {
            exteriorArcRadius += this.leftSideBaseboard.getThickness();
          }
          if (this.rightSideBaseboard != null) {
            interiorArcRadius -= this.rightSideBaseboard.getThickness();
          }
        } else {
          if (this.leftSideBaseboard != null) {
            interiorArcRadius -= this.leftSideBaseboard.getThickness();
          }
          if (this.rightSideBaseboard != null) {
            exteriorArcRadius += this.rightSideBaseboard.getThickness();
          }
        }
      }
      List wallPoints = new ArrayList((angleStepCount + 2) * 2);      
      if (this.symmetric) {
        if (Math.abs(this.arcExtent - angleStepCount * angleDelta) > 1E-6) {
          angleDelta = this.arcExtent / ++angleStepCount;
        }
        for (int i = 0; i <= angleStepCount; i++) {
          computeRoundWallShapePoint(wallPoints, startAngle + this.arcExtent - i * angleDelta, i, angleDelta, 
              arcCircleCenter, exteriorArcRadius, interiorArcRadius);
        }
      } else {
        // Don't change the way walls were computed in version 3.0 to ensure they exactly look the same
        // (as symmetric has no API to modify its value, this case may happen  only for unserialized walls) 
        int i = 0;
        for (float angle = this.arcExtent; angleDelta > 0 ? angle >= angleDelta * 0.1f : angle <= -angleDelta * 0.1f; angle -= angleDelta, i++) {
          computeRoundWallShapePoint(wallPoints, startAngle + angle, i, angleDelta, 
              arcCircleCenter, exteriorArcRadius, interiorArcRadius);
        }
        computeRoundWallShapePoint(wallPoints, startAngle, i, angleDelta, 
            arcCircleCenter, exteriorArcRadius, interiorArcRadius);
      }
      return wallPoints.toArray(new float [wallPoints.size()][]);
    } else { 
      double angle = Math.atan2(this.yEnd - this.yStart, 
                                this.xEnd - this.xStart);
      float sin = (float)Math.sin(angle);
      float cos = (float)Math.cos(angle);
      float leftSideTickness = this.thickness / 2;
      if (includeBaseboards && this.leftSideBaseboard != null) {
        leftSideTickness += this.leftSideBaseboard.getThickness();
      }
      float leftSideDx = sin * leftSideTickness;
      float leftSideDy = cos * leftSideTickness;
      float rightSideTickness = this.thickness / 2;
      if (includeBaseboards && this.rightSideBaseboard != null) {
        rightSideTickness += this.rightSideBaseboard.getThickness();
      }
      float rightSideDx = sin * rightSideTickness;
      float rightSideDy = cos * rightSideTickness;
      return new float [][] {
          {this.xStart + leftSideDx, this.yStart - leftSideDy},
          {this.xEnd   + leftSideDx, this.yEnd   - leftSideDy},
          {this.xEnd   - rightSideDx, this.yEnd   + rightSideDy},
          {this.xStart - rightSideDx, this.yStart + rightSideDy}};
    }
  }

  /**
   * Computes the exterior and interior arc points of a round wall at the given index.
   */  
  private void computeRoundWallShapePoint(List wallPoints, float angle, int index, float angleDelta, 
                                          float [] arcCircleCenter, float exteriorArcRadius, float interiorArcRadius) {
    double cos = Math.cos(angle);
    double sin = Math.sin(angle);
    float [] interiorArcPoint = new float [] {(float)(arcCircleCenter [0] + interiorArcRadius * cos), 
                                              (float)(arcCircleCenter [1] - interiorArcRadius * sin)};
    float [] exteriorArcPoint = new float [] {(float)(arcCircleCenter [0] + exteriorArcRadius * cos), 
                                              (float)(arcCircleCenter [1] - exteriorArcRadius * sin)};
    if (angleDelta > 0) {
      wallPoints.add(index, interiorArcPoint);
      wallPoints.add(wallPoints.size() - 1 - index, exteriorArcPoint);
    } else {
      wallPoints.add(index, exteriorArcPoint);
      wallPoints.add(wallPoints.size() - 1 - index, interiorArcPoint);
    }
  }
  
  /**
   * Compute the intersection between the line that joins point1 to point2
   * and the line that joins point3 and point4, and stores the result 
   * in point1.
   */
  private void computeIntersection(float [] point1, float [] point2, 
                                   float [] point3, float [] point4, float limit) {
    float alpha1 = (point2 [1] - point1 [1]) / (point2 [0] - point1 [0]);
    float alpha2 = (point4 [1] - point3 [1]) / (point4 [0] - point3 [0]);
    // If the two lines are not parallel
    if (alpha1 != alpha2) {
      float x = point1 [0];
      float y = point1 [1];
      
      // If first line is vertical
      if (Math.abs(alpha1) > 4000)  {
        if (Math.abs(alpha2) < 4000) {
          x = point1 [0];
          float beta2  = point4 [1] - alpha2 * point4 [0];
          y = alpha2 * x + beta2;
        }
      // If second line is vertical
      } else if (Math.abs(alpha2) > 4000) {
        if (Math.abs(alpha1) < 4000) {
          x = point3 [0];
          float beta1  = point2 [1] - alpha1 * point2 [0];
          y = alpha1 * x + beta1;
        }
      } else {
        boolean sameSignum = Math.signum(alpha1) == Math.signum(alpha2);
        if (Math.abs(alpha1 - alpha2) > 1E-5
            && (!sameSignum || (Math.abs(alpha1) > Math.abs(alpha2)   ? alpha1 / alpha2   : alpha2 / alpha1) > 1.004)) {
          float beta1 = point2 [1] - alpha1 * point2 [0];
          float beta2 = point4 [1] - alpha2 * point4 [0];
          x = (beta2 - beta1) / (alpha1 - alpha2);
          y = alpha1 * x + beta1;
        } 
      }
      
      if (Point2D.distanceSq(x, y, point1 [0], point1 [1]) < limit * limit) {
        point1 [0] = x;
        point1 [1] = y;
      }
    }
  }
  
  /**
   * Returns true if this wall intersects
   * with the horizontal rectangle which opposite corners are at points
   * (x0, y0) and (x1, y1).
   */
  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(false).intersects(rectangle);
  }
  
  /**
   * Returns true if this wall contains the point at (x, y)
   * not including its baseboards, with a given margin.
   */
  public boolean containsPoint(float x, float y, float margin) {
    return containsPoint(x, y, false, margin);
  }
  
  /**
   * Returns true if this wall contains the point at (x, y)
   * possibly including its baseboards, with a given margin.
   * @since 5.0
   */
  public boolean containsPoint(float x, float y, boolean includeBaseboards, float margin) {
    return containsShapeAtWithMargin(getShape(includeBaseboards), x, y, margin);
  }
  
  /**
   * Returns true if this wall start line contains 
   * the point at (x, y)
   * with a given margin around the wall start line.
   */
  public boolean containsWallStartAt(float x, float y, float margin) {
    float [][] wallPoints = getPoints();
    Line2D startLine = new Line2D.Float(wallPoints [0][0], wallPoints [0][1], 
        wallPoints [wallPoints.length - 1][0], wallPoints [wallPoints.length - 1][1]);
    return containsShapeAtWithMargin(startLine, x, y, margin);
  }
  
  /**
   * Returns true if this wall end line contains 
   * the point at (x, y)
   * with a given margin around the wall end line.
   */
  public boolean containsWallEndAt(float x, float y, float margin) {
    float [][] wallPoints = getPoints();
    Line2D endLine = new Line2D.Float(wallPoints [wallPoints.length / 2 - 1][0], wallPoints [wallPoints.length / 2 - 1][1], 
        wallPoints [wallPoints.length / 2][0], wallPoints [wallPoints.length / 2][1]); 
    return containsShapeAtWithMargin(endLine, x, y, margin);
  }

  /**
   * Returns true if shape contains 
   * the point at (x, y)
   * with a given margin.
   */
  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 shape matching this wall.
   */
  private Shape getShape(boolean includeBaseboards) {
    float [][] wallPoints = getPoints(includeBaseboards);
    GeneralPath wallPath = new GeneralPath();
    wallPath.moveTo(wallPoints [0][0], wallPoints [0][1]);
    for (int i = 1; i < wallPoints.length; i++) {
      wallPath.lineTo(wallPoints [i][0], wallPoints [i][1]);
    }
    wallPath.closePath();
    return wallPath;
  }
  
  /**
   * Returns a clone of the walls list. All existing walls 
   * are copied and their wall at start and end point are set with copied
   * walls only if they belong to the returned list.
   */
  public static List clone(List walls) {
    ArrayList wallsCopy = new ArrayList(walls.size());
    // Clone walls
    for (Wall wall : walls) {
      wallsCopy.add(wall.clone());      
    }
    // Update walls at start and end point
    for (int i = 0; i < walls.size(); i++) {
      Wall wall = walls.get(i);
      int wallAtStartIndex = walls.indexOf(wall.getWallAtStart());
      if (wallAtStartIndex != -1) {
        wallsCopy.get(i).setWallAtStart(wallsCopy.get(wallAtStartIndex));
      }
      int wallAtEndIndex = walls.indexOf(wall.getWallAtEnd());
      if (wallAtEndIndex != -1) {
        wallsCopy.get(i).setWallAtEnd(wallsCopy.get(wallAtEndIndex));
      }
    }
    return wallsCopy;
  }

  /**
   * Moves this wall of (dx, dy) units.
   */
  public void move(float dx, float dy) {
    setXStart(getXStart() + dx);
    setYStart(getYStart() + dy);
    setXEnd(getXEnd() + dx);
    setYEnd(getYEnd() + dy);
  }
  
  /**
   * Returns a clone of this wall expected 
   * its wall at start and wall at end aren't copied.
   */
  @Override
  public Wall clone() {
    Wall clone = (Wall)super.clone();
    clone.propertyChangeSupport = new PropertyChangeSupport(clone);
    clone.wallAtStart = null;
    clone.wallAtEnd = null;
    clone.level = null;
    clone.pointsCache = null;
    clone.pointsIncludingBaseboardsCache = null;
    return clone;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy