View Javadoc
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 }