1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.geometry.euclidean.twod.shape;
18
19 import java.text.MessageFormat;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.List;
23 import java.util.stream.Collectors;
24
25 import org.apache.commons.geometry.core.Transform;
26 import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
27 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
28 import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
29 import org.apache.commons.geometry.euclidean.twod.Lines;
30 import org.apache.commons.geometry.euclidean.twod.Vector2D;
31 import org.apache.commons.geometry.euclidean.twod.rotation.Rotation2D;
32 import org.apache.commons.numbers.core.Precision;
33
34 /** Class representing parallelograms, i.e. quadrilaterals with two pairs of parallel sides.
35 * @see <a href="https://en.wikipedia.org/wiki/Parallelogram">Parallelogram</a>
36 */
37 public final class Parallelogram extends ConvexArea {
38
39 /** Vertices defining a square with sides of length 1 centered on the origin. */
40 private static final List<Vector2D> UNIT_SQUARE_VERTICES = Arrays.asList(
41 Vector2D.of(-0.5, -0.5),
42 Vector2D.of(0.5, -0.5),
43 Vector2D.of(0.5, 0.5),
44 Vector2D.of(-0.5, 0.5)
45 );
46
47 /** Simple constructor. Callers are responsible for ensuring that the given path
48 * represents a parallelogram. No validation is performed.
49 * @param boundaries the boundaries of the parallelogram; this must be a list
50 * with 4 elements
51 */
52 private Parallelogram(final List<LineConvexSubset> boundaries) {
53 super(boundaries);
54 }
55
56 /** Return a new instance representing a unit square centered on the origin.
57 * The vertices of this square are:
58 * <pre>
59 * [
60 * (-0.5 -0.5),
61 * (0.5, -0.5),
62 * (0.5, 0.5),
63 * (-0.5, 0.5)
64 * ]
65 * </pre>
66 * @param precision precision context used to construct boundaries
67 * @return a new instance representing a unit square centered on the origin
68 */
69 public static Parallelogram unitSquare(final Precision.DoubleEquivalence precision) {
70 return fromTransformedUnitSquare(AffineTransformMatrix2D.identity(), precision);
71 }
72
73 /** Return a new instance representing an axis-aligned rectangle. The points {@code a}
74 * and {@code b} are taken to represent opposite corner points in the rectangle and may be specified in
75 * any order.
76 * @param a first corner point in the rectangle (opposite of {@code b})
77 * @param b second corner point in the rectangle (opposite of {@code a})
78 * @param precision precision context used to construct boundaries
79 * @return a new instance representing an axis-aligned rectangle
80 * @throws IllegalArgumentException if the length of any side of the parallelogram is zero,
81 * as determined by the given precision context
82 */
83 public static Parallelogram axisAligned(final Vector2D a, final Vector2D b,
84 final Precision.DoubleEquivalence precision) {
85
86 final double minX = Math.min(a.getX(), b.getX());
87 final double maxX = Math.max(a.getX(), b.getX());
88
89 final double minY = Math.min(a.getY(), b.getY());
90 final double maxY = Math.max(a.getY(), b.getY());
91
92 final double xDelta = maxX - minX;
93 final double yDelta = maxY - minY;
94
95 final Vector2D scale = Vector2D.of(xDelta, yDelta);
96 final Vector2D position = Vector2D.of(
97 (0.5 * xDelta) + minX,
98 (0.5 * yDelta) + minY
99 );
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 }