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}