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