001/**
002 * Plane -- A concrete 2D plane in nD space.
003 *
004 * Copyright (C) 2009-2016, Joseph A. Huwaldt. All rights reserved.
005 *
006 * This library is free software; you can redistribute it and/or modify it under the terms
007 * of the GNU Lesser General Public License as published by the Free Software Foundation;
008 * either version 2.1 of the License, or (at your option) any later version.
009 *
010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
012 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public License along with
015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place -
016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html
017 */
018package geomss.geom;
019
020import jahuwaldt.js.param.Parameter;
021import java.text.MessageFormat;
022import java.util.List;
023import java.util.Objects;
024import static java.util.Objects.requireNonNull;
025import javax.measure.converter.ConversionException;
026import javax.measure.quantity.Dimensionless;
027import javax.measure.quantity.Length;
028import javax.measure.unit.Unit;
029import javolution.context.ImmortalContext;
030import javolution.context.ObjectFactory;
031import javolution.lang.MathLib;
032import javolution.lang.ValueType;
033import javolution.util.FastTable;
034import javolution.xml.XMLFormat;
035import javolution.xml.stream.XMLStreamException;
036import org.apache.commons.math3.linear.MatrixUtils;
037import org.apache.commons.math3.linear.RealMatrix;
038import org.apache.commons.math3.linear.SingularValueDecomposition;
039import org.jscience.mathematics.number.Float64;
040import org.jscience.mathematics.vector.Float64Vector;
041
042/**
043 * A concrete 2D plane in n-dimensional space.
044 * 
045 * <p> Modified by: Joseph A. Huwaldt </p>
046 * 
047 * @author Joseph A. Huwaldt, Date: June 14, 2009
048 * @version September 12, 2016
049 */
050@SuppressWarnings({"serial", "CloneableImplementsClone"})
051public final class Plane extends GeomPlane implements ValueType {
052
053    //  Smallest roundoff in quantities near 0, EPS, such that 0 + EPS > 0
054    private static final double EPS = Parameter.EPS;
055    private static final double SQRT_EPS = Parameter.SQRT_EPS;
056
057    ///////////////////////
058    // Factory creation. //
059    ///////////////////////
060    private Plane() { }
061
062    private static final ObjectFactory<Plane> FACTORY = new ObjectFactory<Plane>() {
063        @Override
064        protected Plane create() {
065            return new Plane();
066        }
067
068        @Override
069        protected void cleanup(Plane obj) {
070            obj.reset();
071            obj._n = null;
072            obj._const = null;
073            obj._refPoint = null;
074        }
075    };
076
077    private static Plane copyOf(Plane original) {
078        Plane o = FACTORY.object();
079        o._n = original._n.copy();
080        o._const = original._const.copy();
081        o._refPoint = original._refPoint.copy();
082        original.copyState(o);
083        return o;
084    }
085
086    private static Plane newInstance() {
087        return FACTORY.object();
088    }
089
090    /**
091     * A 3D plane that represents the YZ plane through the origin.
092     */
093    private static final Plane YZ;
094
095    /**
096     * A 3D plane that represents the XZ plane through the origin.
097     */
098    private static final Plane XZ;
099
100    /**
101     * A 3D plane that represents the XY plane through the origin.
102     */
103    private static final Plane XY;
104
105    static {
106        ImmortalContext.enter();
107        try {
108            YZ = Plane.newInstance();
109            YZ._n = Vector.valueOf(1, 0, 0);
110            YZ._const = Parameter.ZERO_LENGTH;
111            YZ._refPoint = YZ.calcRefPoint();
112
113            XZ = Plane.newInstance();
114            XZ._n = Vector.valueOf(0, 1, 0);
115            XZ._const = Parameter.ZERO_LENGTH;
116            XZ._refPoint = XZ.calcRefPoint();
117
118            XY = Plane.newInstance();
119            XY._n = Vector.valueOf(0, 0, 1);
120            XY._const = Parameter.ZERO_LENGTH;
121            XY._refPoint = XY.calcRefPoint();
122        } finally {
123            ImmortalContext.exit();
124        }
125    }
126
127    /**
128     * Returns a 3D plane that represents the YZ plane through the origin.
129     *
130     * @return A 3D plane that represents the YZ plane through the origin.
131     */
132    public static Plane getYZ() {
133        return YZ.copy();
134    }
135
136    /**
137     * Return a 3D plane that represents the XZ plane through the origin.
138     *
139     * @return A 3D plane that represents the XZ plane through the origin.
140     */
141    public static Plane getXZ() {
142        return XZ.copy();
143    }
144
145    /**
146     * Return a 3D plane that represents the XY plane through the origin.
147     *
148     * @return A 3D plane that represents the XY plane through the origin.
149     */
150    public static Plane getXY() {
151        return XY.copy();
152    }
153
154    /**
155     * The normal vector for the plane.
156     */
157    private GeomVector<Dimensionless> _n;
158
159    /**
160     * The plane offset.
161     */
162    private Parameter<Length> _const;
163
164    /**
165     * A reference point for drawing this plane.
166     */
167    private Point _refPoint;
168
169    /**
170     * Returns a <code>Plane</code> instance with the specified 3D plane equation values.
171     * The plane equation is:  <code>A*x + B*y + C*z = D</code>.
172     *
173     * @param A The coefficient on the X axis term.
174     * @param B The coefficient on the Y axis term.
175     * @param C The coefficient on the Z axis term.
176     * @param D The constant term ((normal DOT p0) where p0 is a point in the plane). May
177     *          not be null.
178     * @return the plane having the specified values.
179     */
180    public static Plane valueOf(double A, double B, double C, Parameter<Length> D) {
181        requireNonNull(D);
182        Plane P = newInstance();
183        Vector n = Vector.valueOf(A, B, C);
184        P._n = n.toUnitVector();
185        P._const = D.divide(n.magValue());
186        P._refPoint = P.calcRefPoint();
187        P._n.setOrigin(P._refPoint);
188        return P;
189    }
190
191    /**
192     * Returns a <code>Plane</code> instance with the specified normal vector and plane
193     * equation constant.
194     *
195     * @param normal   The unit normal of the plane. May not be null.
196     * @param constant The constant term of the plane equation ((normal DOT p0) where p0
197     *                 is a point in the plane). May not be null.
198     * @return the plane having the specified values.
199     * @throws DimensionException if the input normal vector is not at least 3
200     * dimensional.
201     */
202    public static Plane valueOf(GeomVector<Dimensionless> normal, Parameter<Length> constant) {
203        requireNonNull(constant);
204        int numDims = normal.getPhyDimension();
205        if (numDims < 3)
206            throw new DimensionException(
207                    MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"),
208                            "input normal vector", numDims));
209
210        Plane P = newInstance();
211        P._n = normal.toUnitVector();
212        P._const = constant;
213        P._refPoint = P.calcRefPoint();
214        P._n.setOrigin(P._refPoint);
215        return P;
216    }
217
218    /**
219     * Returns a <code>Plane</code> instance with the specified normal vector and plane
220     * equation constant.
221     *
222     * @param normal The unit normal of the plane. May not be null.
223     * @param p      A point in the plane (the reference point for the plane). May not be
224     *               null.
225     * @return the plane having the specified values.
226     * @throws DimensionException if the input normal vector is not at least 3
227     * dimensional.
228     */
229    public static Plane valueOf(GeomVector<Dimensionless> normal, Point p) {
230        int numDims = normal.getPhyDimension();
231        if (numDims < 3)
232            throw new DimensionException(
233                    MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"),
234                            "input normal vector", numDims));
235
236        //  Convert all the points to the highest dimension between them.
237        if (p.getPhyDimension() > numDims)
238            numDims = p.getPhyDimension();
239        p = p.toDimension(numDims);
240        normal = normal.toDimension(numDims);
241
242        Plane P = newInstance();
243        P._n = normal.toUnitVector();
244        P._const = (Parameter<Length>)P._n.dot(p.toGeomVector());
245        P._refPoint = p;
246        P._n.setOrigin(p);
247
248        return P;
249    }
250
251    /**
252     * Returns a <code>Plane</code> instance that contains the specified 3 independent
253     * points.
254     *
255     * @param p A point in the plane (not equal to one of the other two). May not be null.
256     * @param q A point in the plane (not equal to one of the other two). May not be null.
257     * @param r A point in the plane (not equal to one of the other two). May not be null.
258     * @return The plane containing the specified points. The plane will match the
259     *         dimensions of the highest dimension point passed to this method.
260     * @throws DimensionException if one of the input points does not have at least 3
261     * physical dimensions.
262     * @throws IllegalArgumentException if any of the input points are equal to the
263     * others.
264     */
265    public static Plane valueOf(GeomPoint p, GeomPoint q, GeomPoint r) throws DimensionException {
266
267        //  Convert all the points to the highest dimension between them.
268        int numDims = GeomUtil.maxPhyDimension(p, q, r);
269        if (numDims < 3)
270            throw new DimensionException(
271                    MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"),
272                            "input points", numDims));
273
274        p = p.toDimension(numDims);
275        q = q.toDimension(numDims);
276        r = r.toDimension(numDims);
277
278        Vector<Length> PQ = q.minus(p).toGeomVector();
279        Vector<Length> PR = r.minus(p).toGeomVector();
280        if (PQ.magValue() < SQRT_EPS || PR.magValue() < SQRT_EPS)
281            throw new IllegalArgumentException(RESOURCES.getString("pointsEqualErr"));
282
283        Vector PQcrossPR = PQ.cross(PR);
284        if (PQcrossPR.magValue() < SQRT_EPS)
285            throw new IllegalArgumentException(RESOURCES.getString("pointsEqualErr"));
286
287        Point refP = p.immutable();
288        Plane P = newInstance();
289        P._n = PQcrossPR.toUnitVector();
290        P._const = (Parameter<Length>)P._n.dot(refP.toGeomVector());
291        P._refPoint = refP;
292        P._n.setOrigin(refP);
293
294        return P;
295    }
296
297    /**
298     * Returns a <code>Plane</code> instance that contains the specified triangle.
299     *
300     * @param tri A triangle that defines the plane. May not be null.
301     * @return The plane containing the specified triangle. The plane will match the
302     *         dimensions of the triangle.
303     * @throws DimensionException if the input triangle does not have at least 3
304     * physical dimensions.
305     * @throws IllegalArgumentException if the triangle is degenerate (has two vertices
306     * equal to each other).
307     */
308    public static Plane valueOf(GeomTriangle tri) throws DimensionException {
309        return valueOf(tri.getP1(), tri.getP2(), tri.getP3());
310    }
311 
312    /**
313     * Return a <code>Plane</code> instance that is fit through the input list of points
314     * resulting in the least-squared orthogonal error between the points and the plane.
315     *
316     * @param points The list of points to fit a plane to.
317     * @return A plane that represents the least squared error fit to the input points.
318     */
319    public static Plane fitPoints(List<? extends GeomPoint> points) {
320        //  Reference: http://mathforum.org/library/drmath/view/63765.html
321
322        int numPoints = points.size();
323        if (numPoints < 3)
324            throw new IllegalArgumentException(MessageFormat.format(
325                    RESOURCES.getString("planeNumPointsErr"), numPoints));
326        if (numPoints == 3)
327            return Plane.valueOf(points.get(0), points.get(1), points.get(2));
328
329        //  Convert the input list of points to a PointString.
330        PointString<? extends GeomPoint> pnts;
331        if (points instanceof PointString)
332            pnts = (PointString)points;
333        else
334            pnts = PointString.valueOf(null, points);
335        int numDims = pnts.getPhyDimension();
336        if (numDims < 3)
337            throw new IllegalArgumentException(MessageFormat.format(
338                    RESOURCES.getString("dimensionNotAtLeast3"), "input points", numDims));
339
340        //  Get the units of the points.
341        Unit refUnit = pnts.getUnit();
342
343        // Find the centroid of all the points.
344        Point centroid = pnts.getAverage();
345
346        //  Form a matrix of input points.
347        double[] tmpArr = new double[numDims];
348        RealMatrix Mmat = MatrixUtils.createRealMatrix(numPoints, numDims);
349        for (int i = 0; i < numPoints; ++i) {
350            GeomPoint point = pnts.get(i).to(refUnit);
351            Mmat.setRow(i, point.toArray(tmpArr));
352        }
353
354        //  Calculate the singular value decomposition.
355        SingularValueDecomposition svd = new SingularValueDecomposition(Mmat);
356
357        //  Get the matrix V of the decomposition. The columns of V are the singular vectors.
358        //  They are sorted from the largest to the smallest singular value.
359        RealMatrix V = svd.getV();
360
361        //  The normal vector corresponds to the smallest singular value from SVD, which is always the last one.
362        Vector n = Vector.valueOf(V.getColumn(numDims - 1));
363
364        //  The best-fit plane is the one passing through the centroid with the
365        //  calculated normal vector.
366        return Plane.valueOf(n, centroid);
367    }
368
369    /**
370     * Calculate and return an arbitrary reference point that is <i>any</i> point in this
371     * plane.
372     */
373    private Point calcRefPoint() {
374
375        //  Find a non-zero axis.
376        int dim = getPhyDimension();
377        int axis = 0;
378        for (; axis < dim; ++axis)
379            if (MathLib.abs(_n.getValue(axis)) >= EPS)
380                break;
381
382        //  Set all axes except the chosen one to zero and solve for the point.
383        Float64 v = Float64.valueOf(_const.getValue() / _n.getValue(axis));
384        FastTable<Float64> values = FastTable.newInstance();
385
386        for (int i = 0; i < dim; ++i) {
387            if (i != axis)
388                values.add(Float64.ZERO);
389            else
390                values.add(v);
391        }
392
393        Point p = Point.valueOf(Float64Vector.valueOf(values), _const.getUnit());
394
395        return p;
396    }
397
398    /**
399     * Return an immutable version of this plane.
400     *
401     * @return An immutable version of this plane.
402     */
403    @Override
404    public Plane immutable() {
405        return this;
406    }
407
408    /**
409     * Recycles a <code>Plane</code> instance immediately (on the stack when executing in
410     * a <code>StackContext</code>).
411     *
412     * @param instance The instance to be recycled.
413     */
414    public static void recycle(Plane instance) {
415        FACTORY.recycle(instance);
416    }
417
418    /**
419     * Returns the number of physical dimensions of the geometry element.
420     *
421     * @return The number of physical dimensions of the geometry element.
422     */
423    @Override
424    public int getPhyDimension() {
425        return _n.getPhyDimension();
426    }
427
428    /**
429     * Return the equivalent of this plane converted to the specified number of physical
430     * dimensions. If the number of dimensions is greater than this element, then zeros
431     * are added to the additional dimensions. If the number of dimensions is less than
432     * this element, then the extra dimensions are simply dropped (truncated). If the new
433     * dimensions are the same as the dimension of this element, then this element is
434     * simply returned.
435     *
436     * @param newDim The dimension of the plane to return.
437     * @return A copy of this plane converted to the new dimensions.
438     */
439    @Override
440    public Plane toDimension(int newDim) {
441        if (newDim < 3)
442            throw new DimensionException(
443                    MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"),
444                            "input normal vector", newDim));
445        int thisDim = this.getPhyDimension();
446        if (newDim == thisDim)
447            return this;
448
449        Plane P = Plane.valueOf(getNormal().toDimension(newDim), getRefPoint().toDimension(newDim));
450        return P;
451    }
452
453    /**
454     * Return the normal vector for the plane. The normal vector is a unit vector that is
455     * perpendicular to the plane.
456     *
457     * @return The normal vector for the plane.
458     */
459    @Override
460    public GeomVector<Dimensionless> getNormal() {
461        return _n.copy();
462    }
463
464    /**
465     * Return the constant term of the plane equation (e.g.: "D" for a 3D plane:
466     * <code>A*x + B*y + C*z = D</code>).
467     *
468     * @return The constant term of the plane equation for this plane.
469     */
470    @Override
471    public Parameter<Length> getConstant() {
472        return _const;
473    }
474
475    /**
476     * Return the reference point for this plane. The reference point is an arbitrary
477     * point that is contained in the plane and is used as a reference when drawing the
478     * plane.
479     *
480     * @return The reference point for this plane.
481     */
482    @Override
483    public Point getRefPoint() {
484        return _refPoint;
485    }
486
487    /**
488     * Returns the unit in which the geometry in this element are stated.
489     *
490     * @return The unit in which the geometry in this element are stated.
491     */
492    @Override
493    public Unit<Length> getUnit() {
494        return _const.getUnit();
495    }
496
497    /**
498     * Returns the equivalent to this element but stated in the specified unit.
499     *
500     * @param unit the length unit of the element to be returned. May not be null.
501     * @return an equivalent to this element but stated in the specified unit.
502     * @throws ConversionException if the the input unit is not a length unit.
503     */
504    @Override
505    public Plane to(Unit<Length> unit) {
506        if (unit.equals(getUnit()))
507            return this;
508        Plane newPlane = Plane.valueOf(_n, _const.to(unit));
509        //newPlane.setRefPoint(_refPoint.to(unit));
510        copyState(newPlane);
511        return newPlane;
512    }
513
514    /**
515     * Returns a copy of this Plane instance
516     * {@link javolution.context.AllocatorContext allocated} by the calling thread
517     * (possibly on the stack).
518     *
519     * @return an identical and independent copy of this point.
520     */
521    @Override
522    public Plane copy() {
523        return copyOf(this);
524    }
525
526    /**
527     * Return a copy of this object with any transformations or subranges removed
528     * (applied).
529     *
530     * @return A copy of this object with any transformations or subranges removed.
531     */
532    @Override
533    public Plane copyToReal() {
534        return copy();
535    }
536
537    /**
538     * Compares this Plane against the specified object for strict equality (same values
539     * and same units).
540     *
541     * @param obj the object to compare with.
542     * @return <code>true</code> if this object is identical to that object;
543     *         <code>false</code> otherwise.
544     */
545    @Override
546    public boolean equals(Object obj) {
547        if (this == obj)
548            return true;
549        if ((obj == null) || (obj.getClass() != this.getClass()))
550            return false;
551
552        Plane that = (Plane)obj;
553        return this._n.equals(that._n)
554                && this._const.equals(that._const)
555                && super.equals(obj);
556    }
557
558    /**
559     * Returns the hash code for this parameter.
560     *
561     * @return the hash code value.
562     */
563    @Override
564    public int hashCode() {
565        return 31*super.hashCode() + Objects.hash(_n, _const);
566    }
567
568    /**
569     * Holds the default XML representation for this object.
570     */
571    @SuppressWarnings("FieldNameHidesFieldInSuperclass")
572    protected static final XMLFormat<Plane> XML = new XMLFormat<Plane>(Plane.class) {
573
574        @Override
575        public Plane newInstance(Class<Plane> cls, InputElement xml) throws XMLStreamException {
576            return FACTORY.object();
577        }
578
579        @Override
580        public void read(InputElement xml, Plane obj) throws XMLStreamException {
581            GeomPlane.XML.read(xml, obj);     // Call parent read.
582
583            obj._n = (GeomVector<Dimensionless>)xml.getNext();
584            obj._const = (Parameter)xml.getNext();
585
586            obj._refPoint = obj._n.getOrigin();
587        }
588
589        @Override
590        public void write(Plane obj, OutputElement xml) throws XMLStreamException {
591            GeomPlane.XML.write(obj, xml);    // Call parent write.
592
593            xml.add(obj._n);
594            xml.add(obj._const);
595
596        }
597    };
598
599}