001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.geometry.euclidean.twod.shape; 018 019import java.text.MessageFormat; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.List; 023import java.util.stream.Collectors; 024 025import org.apache.commons.geometry.core.Transform; 026import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D; 027import org.apache.commons.geometry.euclidean.twod.ConvexArea; 028import org.apache.commons.geometry.euclidean.twod.LineConvexSubset; 029import org.apache.commons.geometry.euclidean.twod.Lines; 030import org.apache.commons.geometry.euclidean.twod.Vector2D; 031import org.apache.commons.geometry.euclidean.twod.rotation.Rotation2D; 032import org.apache.commons.numbers.core.Precision; 033 034/** Class representing parallelograms, i.e. quadrilaterals with two pairs of parallel sides. 035 * @see <a href="https://en.wikipedia.org/wiki/Parallelogram">Parallelogram</a> 036 */ 037public final class Parallelogram extends ConvexArea { 038 039 /** Vertices defining a square with sides of length 1 centered on the origin. */ 040 private static final List<Vector2D> UNIT_SQUARE_VERTICES = Arrays.asList( 041 Vector2D.of(-0.5, -0.5), 042 Vector2D.of(0.5, -0.5), 043 Vector2D.of(0.5, 0.5), 044 Vector2D.of(-0.5, 0.5) 045 ); 046 047 /** Simple constructor. Callers are responsible for ensuring that the given path 048 * represents a parallelogram. No validation is performed. 049 * @param boundaries the boundaries of the parallelogram; this must be a list 050 * with 4 elements 051 */ 052 private Parallelogram(final List<LineConvexSubset> boundaries) { 053 super(boundaries); 054 } 055 056 /** Return a new instance representing a unit square centered on the origin. 057 * The vertices of this square are: 058 * <pre> 059 * [ 060 * (-0.5 -0.5), 061 * (0.5, -0.5), 062 * (0.5, 0.5), 063 * (-0.5, 0.5) 064 * ] 065 * </pre> 066 * @param precision precision context used to construct boundaries 067 * @return a new instance representing a unit square centered on the origin 068 */ 069 public static Parallelogram unitSquare(final Precision.DoubleEquivalence precision) { 070 return fromTransformedUnitSquare(AffineTransformMatrix2D.identity(), precision); 071 } 072 073 /** Return a new instance representing an axis-aligned rectangle. The points {@code a} 074 * and {@code b} are taken to represent opposite corner points in the rectangle and may be specified in 075 * any order. 076 * @param a first corner point in the rectangle (opposite of {@code b}) 077 * @param b second corner point in the rectangle (opposite of {@code a}) 078 * @param precision precision context used to construct boundaries 079 * @return a new instance representing an axis-aligned rectangle 080 * @throws IllegalArgumentException if the length of any side of the parallelogram is zero, 081 * as determined by the given precision context 082 */ 083 public static Parallelogram axisAligned(final Vector2D a, final Vector2D b, 084 final Precision.DoubleEquivalence precision) { 085 086 final double minX = Math.min(a.getX(), b.getX()); 087 final double maxX = Math.max(a.getX(), b.getX()); 088 089 final double minY = Math.min(a.getY(), b.getY()); 090 final double maxY = Math.max(a.getY(), b.getY()); 091 092 final double xDelta = maxX - minX; 093 final double yDelta = maxY - minY; 094 095 final Vector2D scale = Vector2D.of(xDelta, yDelta); 096 final Vector2D position = Vector2D.of( 097 (0.5 * xDelta) + minX, 098 (0.5 * yDelta) + minY 099 ); 100 101 return builder(precision) 102 .setScale(scale) 103 .setPosition(position) 104 .build(); 105 } 106 107 /** Create a new instance by transforming a unit square centered at the origin. The vertices 108 * of this input square are: 109 * <pre> 110 * [ 111 * (-0.5 -0.5), 112 * (0.5, -0.5), 113 * (0.5, 0.5), 114 * (-0.5, 0.5) 115 * ] 116 * </pre> 117 * @param transform the transform to apply to the unit square 118 * @param precision precision context used to construct boundaries 119 * @return a new instance constructed by transforming the unit square 120 * @throws IllegalArgumentException if the length of any side of the parallelogram is zero, 121 * as determined by the given precision context 122 */ 123 public static Parallelogram fromTransformedUnitSquare(final Transform<Vector2D> transform, 124 final Precision.DoubleEquivalence precision) { 125 126 final List<Vector2D> vertices = UNIT_SQUARE_VERTICES.stream() 127 .map(transform).collect(Collectors.toList()); 128 129 final int len = vertices.size(); 130 final boolean preservesOrientation = transform.preservesOrientation(); 131 132 final List<LineConvexSubset> boundaries = new ArrayList<>(UNIT_SQUARE_VERTICES.size()); 133 134 Vector2D p0; 135 Vector2D p1; 136 LineConvexSubset boundary; 137 for (int i = 0; i < len; ++i) { 138 p0 = vertices.get(i); 139 p1 = vertices.get((i + 1) % len); 140 141 if (precision.eqZero(p0.distance(p1))) { 142 throw new IllegalArgumentException(MessageFormat.format( 143 "Parallelogram has zero size: vertices {0} and {1} are equivalent", p0, p1)); 144 } 145 146 boundary = preservesOrientation ? 147 Lines.segmentFromPoints(p0, p1, precision) : 148 Lines.segmentFromPoints(p1, p0, precision); 149 150 boundaries.add(boundary); 151 } 152 153 return new Parallelogram(boundaries); 154 } 155 156 /** Return a new {@link Builder} instance to use for constructing parallelograms. 157 * @param precision precision context used to create boundaries 158 * @return a new {@link Builder} instance 159 */ 160 public static Builder builder(final Precision.DoubleEquivalence precision) { 161 return new Builder(precision); 162 } 163 164 /** Class designed to aid construction of {@link Parallelogram} instances. Parallelograms are constructed 165 * by transforming the vertices of a unit square centered at the origin with a transform built from 166 * the values configured here. The transformations applied are <em>scaling</em>, <em>rotation</em>, 167 * and <em>translation</em>, in that order. When applied in this order, the scale factors determine 168 * the width and height of the parallelogram, the rotation determines the orientation, and the translation 169 * determines the position of the center point. 170 */ 171 public static final class Builder { 172 173 /** Amount to scale the parallelogram. */ 174 private Vector2D scale = Vector2D.of(1, 1); 175 176 /** The rotation of the parallelogram. */ 177 private Rotation2D rotation = Rotation2D.identity(); 178 179 /** Amount to translate the parallelogram. */ 180 private Vector2D position = Vector2D.ZERO; 181 182 /** Precision context used to construct boundaries. */ 183 private final Precision.DoubleEquivalence precision; 184 185 /** Construct a new instance configured with the given precision context. 186 * @param precision precision context used to create boundaries 187 */ 188 private Builder(final Precision.DoubleEquivalence precision) { 189 this.precision = precision; 190 } 191 192 /** Set the center position of the created parallelogram. 193 * @param pos center position of the created parallelogram 194 * @return this instance 195 */ 196 public Builder setPosition(final Vector2D pos) { 197 this.position = pos; 198 return this; 199 } 200 201 /** Set the scaling for the created parallelogram. The scale 202 * values determine the lengths of the respective sides in the 203 * created parallelogram. 204 * @param scaleFactors scale factors 205 * @return this instance 206 */ 207 public Builder setScale(final Vector2D scaleFactors) { 208 this.scale = scaleFactors; 209 return this; 210 } 211 212 /** Set the scaling for the created parallelogram. The scale 213 * values determine the lengths of the respective sides in the 214 * created parallelogram. 215 * @param x x scale factor 216 * @param y y scale factor 217 * @return this instance 218 */ 219 public Builder setScale(final double x, final double y) { 220 return setScale(Vector2D.of(x, y)); 221 } 222 223 /** Set the scaling for the created parallelogram. The given scale 224 * factor is applied to both the x and y directions. 225 * @param scaleFactor scale factor for x and y directions 226 * @return this instance 227 */ 228 public Builder setScale(final double scaleFactor) { 229 return setScale(scaleFactor, scaleFactor); 230 } 231 232 /** Set the rotation of the created parallelogram. 233 * @param rot the rotation of the created parallelogram 234 * @return this instance 235 */ 236 public Builder setRotation(final Rotation2D rot) { 237 this.rotation = rot; 238 return this; 239 } 240 241 /** Set the rotation of the created parallelogram such that the 242 * relative x-axis of the shape points in the given direction. 243 * @param xDirection the direction of the relative x-axis 244 * @return this instance 245 * @throws IllegalArgumentException if the given vector cannot be normalized 246 * @see #setRotation(Rotation2D) 247 */ 248 public Builder setXDirection(final Vector2D xDirection) { 249 return setRotation( 250 Rotation2D.createVectorRotation(Vector2D.Unit.PLUS_X, xDirection)); 251 } 252 253 /** Set the rotation of the created parallelogram such that the 254 * relative y-axis of the shape points in the given direction. 255 * @param yDirection the direction of the relative y-axis 256 * @return this instance 257 * @throws IllegalArgumentException if the given vector cannot be normalized 258 * @see #setRotation(Rotation2D) 259 */ 260 public Builder setYDirection(final Vector2D yDirection) { 261 return setRotation( 262 Rotation2D.createVectorRotation(Vector2D.Unit.PLUS_Y, yDirection)); 263 } 264 265 /** Build a new parallelogram instance with the values configured in this builder. 266 * @return a new parallelogram instance 267 * @throws IllegalArgumentException if the length of any side of the parallelogram is zero, 268 * as determined by the configured precision context 269 * @see Parallelogram#fromTransformedUnitSquare(Transform, Precision.DoubleEquivalence) 270 */ 271 public Parallelogram build() { 272 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(scale) 273 .rotate(rotation) 274 .translate(position); 275 276 return fromTransformedUnitSquare(transform, precision); 277 } 278 } 279}