001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.math.geometry.shape;
031
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.Iterator;
035import java.util.List;
036
037import org.openimaj.math.geometry.GeometricObject;
038import org.openimaj.math.geometry.line.Line2d;
039import org.openimaj.math.geometry.point.Point2d;
040import org.openimaj.math.geometry.point.Point2dImpl;
041import org.openimaj.math.geometry.shape.util.GrahamScan;
042
043import Jama.Matrix;
044
045/**
046 * A base implementation of a {@link GeometricObject} that is a set of points in
047 * space.
048 * 
049 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
050 * 
051 */
052public class PointList implements GeometricObject, Iterable<Point2d>, Cloneable {
053        /** The points in the {@link PointList} */
054        public List<Point2d> points = new ArrayList<Point2d>();
055
056        /**
057         * Construct a {@link PointList} from points
058         * 
059         * @param points
060         *            the points
061         */
062        public PointList(Point2d... points) {
063                for (final Point2d v : points)
064                        this.points.add(v);
065        }
066
067        /**
068         * Construct a {@link PointList} from points
069         * 
070         * @param points
071         *            the points
072         */
073        public PointList(Collection<? extends Point2d> points) {
074                this(points, false);
075        }
076
077        /**
078         * Construct a {@link PointList} from the points, possibly copying the
079         * points first
080         * 
081         * @param points
082         *            the points
083         * @param copy
084         *            should the points be copied
085         */
086        public PointList(Collection<? extends Point2d> points, boolean copy) {
087                if (!copy)
088                        this.points.addAll(points);
089                else
090                {
091                        for (final Point2d p : points)
092                                this.points.add(new Point2dImpl(p.getX(), p.getY()));
093                }
094        }
095
096        void rotate(Point2d point, Point2d origin, double angle) {
097                final double X = origin.getX() + ((point.getX() - origin.getX()) * Math.cos(angle) -
098                                (point.getY() - origin.getY()) * Math.sin(angle));
099
100                final double Y = origin.getY() + ((point.getX() - origin.getX()) * Math.sin(angle) +
101                                (point.getY() - origin.getY()) * Math.cos(angle));
102
103                point.setX((float) X);
104                point.setY((float) Y);
105        }
106
107        /**
108         * Rotate the {@link PointList} about the given origin with the given angle
109         * (in radians)
110         * 
111         * @param origin
112         *            the origin of the rotation
113         * @param angle
114         *            the angle in radians
115         */
116        public void rotate(Point2d origin, double angle) {
117                for (final Point2d p : points)
118                        rotate(p, origin, angle);
119        }
120
121        /**
122         * Rotate the {@link PointList} about (0,0) with the given angle (in
123         * radians)
124         * 
125         * @param angle
126         *            the angle in radians
127         */
128        public void rotate(double angle) {
129                this.rotate(new Point2dImpl(0, 0), angle);
130        }
131
132        /**
133         * Compute the regular (oriented to the axes) bounding box of the
134         * {@link PointList}.
135         * 
136         * @return the regular bounding box
137         */
138        @Override
139        public Rectangle calculateRegularBoundingBox() {
140                int xmin = Integer.MAX_VALUE, xmax = 0, ymin = Integer.MAX_VALUE, ymax = 0;
141
142                for (final Point2d p : points) {
143                        if (p.getX() < xmin)
144                                xmin = (int) Math.floor(p.getX());
145                        if (p.getX() > xmax)
146                                xmax = (int) Math.ceil(p.getX());
147                        if (p.getY() < ymin)
148                                ymin = (int) Math.floor(p.getY());
149                        if (p.getY() > ymax)
150                                ymax = (int) Math.ceil(p.getY());
151                }
152
153                return new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin);
154        }
155
156        /**
157         * Translate the {@link PointList}s position
158         * 
159         * @param x
160         *            x-translation
161         * @param y
162         *            y-translation
163         */
164        @Override
165        public void translate(float x, float y) {
166                for (final Point2d p : points) {
167                        p.setX(p.getX() + x);
168                        p.setY(p.getY() + y);
169                }
170        }
171
172        /**
173         * Scale the {@link PointList} by the given amount about (0,0). Scalefactors
174         * between 0 and 1 shrink the {@link PointList}.
175         * 
176         * @param sc
177         *            the scale factor.
178         */
179        @Override
180        public void scale(float sc) {
181                for (final Point2d p : points) {
182                        p.setX(p.getX() * sc);
183                        p.setY(p.getY() * sc);
184                }
185        }
186
187        /**
188         * Scale the {@link PointList} only in the x-direction by the given amount
189         * about (0,0). Scale factors between 0 and 1 will shrink the
190         * {@link PointList}
191         * 
192         * @param sc
193         *            The scale factor
194         * @return this {@link PointList}
195         */
196        public PointList scaleX(float sc)
197        {
198                for (final Point2d p : points) {
199                        p.setX(p.getX() * sc);
200                }
201                return this;
202        }
203
204        /**
205         * Scale the {@link PointList} only in the y-direction by the given amount
206         * about (0,0). Scale factors between 0 and 1 will shrink the
207         * {@link PointList}
208         * 
209         * @param sc
210         *            The scale factor
211         * @return this {@link PointList}
212         */
213        public PointList scaleY(float sc)
214        {
215                for (final Point2d p : points) {
216                        p.setY(p.getY() * sc);
217                }
218                return this;
219        }
220
221        /**
222         * Scale the {@link PointList} by the given amount about (0,0). Scale
223         * factors between 0 and 1 shrink the {@link PointList}.
224         * 
225         * @param scx
226         *            the scale factor in the x direction
227         * @param scy
228         *            the scale factor in the y direction.
229         * @return this {@link PointList}
230         */
231        public PointList scaleXY(float scx, float scy)
232        {
233                for (final Point2d p : points) {
234                        p.setX(p.getX() * scx);
235                        p.setY(p.getY() * scy);
236                }
237                return this;
238        }
239
240        /**
241         * Scale the {@link PointList} by the given amount about the given point.
242         * Scalefactors between 0 and 1 shrink the {@link PointList}.
243         * 
244         * @param centre
245         *            the centre of the scaling operation
246         * @param sc
247         *            the scale factor
248         */
249        @Override
250        public void scale(Point2d centre, float sc) {
251                this.translate(-centre.getX(), -centre.getY());
252
253                for (final Point2d p : points) {
254                        p.setX(p.getX() * sc);
255                        p.setY(p.getY() * sc);
256                }
257
258                this.translate(centre.getX(), centre.getY());
259        }
260
261        /**
262         * Scale the {@link PointList} about its centre of gravity. Scalefactors
263         * between 0 and 1 shrink the {@link PointList}.
264         * 
265         * @param sc
266         *            the scale factor
267         */
268        @Override
269        public void scaleCentroid(float sc)
270        {
271                final Point2d cog = calculateCentroid();
272                this.translate(-cog.getX(), -cog.getY());
273                this.scale(sc);
274                this.translate(cog.getX(), cog.getY());
275        }
276
277        /**
278         * Get the centre of gravity of the {@link PointList}
279         * 
280         * @return the centre of gravity of the {@link PointList}
281         */
282        @Override
283        public Point2d calculateCentroid()
284        {
285                float xSum = 0;
286                float ySum = 0;
287
288                int n = 0;
289
290                for (final Point2d p : points) {
291                        xSum += p.getX();
292                        ySum += p.getY();
293                        n++;
294                }
295
296                xSum /= n;
297                ySum /= n;
298
299                return new Point2dImpl(xSum, ySum);
300        }
301
302        /**
303         * @return the minimum x-ordinate of all vertices
304         */
305        @Override
306        public double minX() {
307                return calculateRegularBoundingBox().x;
308        }
309
310        /**
311         * @return the minimum y-ordinate of all vertices
312         */
313        @Override
314        public double minY() {
315                return calculateRegularBoundingBox().y;
316        }
317
318        /**
319         * @return the maximum x-ordinate of all vertices
320         */
321        @Override
322        public double maxX() {
323                final Rectangle r = calculateRegularBoundingBox();
324                return r.x + r.width;
325        }
326
327        /**
328         * @return the maximum y-ordinate of all vertices
329         */
330        @Override
331        public double maxY() {
332                final Rectangle r = calculateRegularBoundingBox();
333                return r.y + r.height;
334        }
335
336        /**
337         * @return the width of the regular bounding box
338         */
339        @Override
340        public double getWidth() {
341                return maxX() - minX();
342        }
343
344        /**
345         * @return the height of the regular bounding box
346         */
347        @Override
348        public double getHeight() {
349                return maxY() - minY();
350        }
351
352        /**
353         * Apply a 3x3 transform matrix to a copy of the {@link PointList} and
354         * return it
355         * 
356         * @param transform
357         *            3x3 transform matrix
358         * @return the transformed {@link PointList}
359         */
360        @Override
361        public PointList transform(Matrix transform) {
362                final List<Point2d> newVertices = new ArrayList<Point2d>();
363
364                for (final Point2d p : points) {
365                        final Matrix p1 = new Matrix(3, 1);
366                        p1.set(0, 0, p.getX());
367                        p1.set(1, 0, p.getY());
368                        p1.set(2, 0, 1);
369
370                        final Matrix p2_est = transform.times(p1);
371
372                        final Point2d out = new Point2dImpl((float) (p2_est.get(0, 0) / p2_est.get(2, 0)),
373                                        (float) (p2_est.get(1, 0) / p2_est.get(2, 0)));
374
375                        newVertices.add(out);
376                }
377
378                return new PointList(newVertices);
379        }
380
381        /**
382         * {@inheritDoc}
383         * 
384         * @see java.lang.Iterable#iterator()
385         */
386        @Override
387        public Iterator<Point2d> iterator()
388        {
389                return points.iterator();
390        }
391
392        @Override
393        public String toString() {
394                return points.toString();
395        }
396
397        /**
398         * Compute the mean of a set of {@link PointList}s. It is assumed that the
399         * number of points in the {@link PointList}s is equal, and that their is a
400         * one-to-one correspondance between the ith point in each list.
401         * 
402         * @param shapes
403         *            the shapes to average
404         * @return the average shape
405         */
406        public static PointList computeMean(Collection<PointList> shapes) {
407                final int npoints = shapes.iterator().next().points.size();
408                final PointList mean = new PointList();
409
410                for (int i = 0; i < npoints; i++)
411                        mean.points.add(new Point2dImpl());
412
413                for (final PointList shape : shapes) {
414                        for (int i = 0; i < npoints; i++) {
415                                final Point2dImpl pt = (Point2dImpl) mean.points.get(i);
416
417                                pt.x += shape.points.get(i).getX();
418                                pt.y += shape.points.get(i).getY();
419                        }
420                }
421
422                for (int i = 0; i < npoints; i++) {
423                        final Point2dImpl pt = (Point2dImpl) mean.points.get(i);
424
425                        pt.x /= shapes.size();
426                        pt.y /= shapes.size();
427                }
428
429                return mean;
430        }
431
432        /**
433         * @return the number of points in the list
434         */
435        public int size() {
436                return points.size();
437        }
438
439        /**
440         * Calculate the intrinsic scale of the shape. This is the RMS distance of
441         * all the points from the centroid.
442         * 
443         * @return the scale of the object.
444         */
445        public float computeIntrinsicScale() {
446                final Point2d cog = this.calculateCentroid();
447                float scale = 0;
448
449                for (final Point2d pt : this) {
450                        final double x = pt.getX() - cog.getX();
451                        final double y = pt.getY() - cog.getY();
452
453                        scale += x * x + y * y;
454                }
455
456                return (float) Math.sqrt(scale / points.size());
457        }
458
459        /**
460         * Get the ith point
461         * 
462         * @param i
463         *            the index of the point
464         * @return the ith point
465         */
466        public Point2d get(int i) {
467                return points.get(i);
468        }
469
470        /**
471         * @return A list of {@link Line2d} assuming the points in this list are
472         *         connected in order
473         */
474        public List<Line2d> getLines() {
475                final List<Line2d> lines = new ArrayList<Line2d>(points.size() - 1);
476
477                for (int i = 1; i < this.points.size(); i++) {
478                        lines.add(new Line2d(
479                                        points.get(i - 1),
480                                        points.get(i)
481                                        ));
482                }
483
484                return lines;
485        }
486
487        /**
488         * @param conns
489         * @return calls {@link PointListConnections#getLines(PointList)} with this
490         */
491        public List<Line2d> getLines(PointListConnections conns) {
492                return conns.getLines(this);
493        }
494
495        @Override
496        public PointList clone() {
497                final PointList p = new PointList();
498                for (final Point2d point2d : this) {
499                        p.points.add(point2d.copy());
500                }
501                return p;
502        }
503
504        /**
505         * Calculate the convex hull of the points using the Graham Scan algorithm.
506         * 
507         * @see GrahamScan
508         * @return the convex hull
509         */
510        public Polygon calculateConvexHull() {
511                return GrahamScan.getConvexHull(this.points);
512        }
513}