Point3D.java

/*
 * Copyright (C) 2017 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.irurueta.geometry;

import java.io.Serializable;
import java.util.Collection;

/**
 * Abstract class defining the base interface that all 3D points should have.
 * 3D points describe points in a 3D space such as the Euclidean space. They can
 * be  implemented either as homogeneous or inhomogeneous points.
 */
public abstract class Point3D implements Serializable, Point<Point3D> {

    /**
     * Defines the threshold used when comparing two values.
     */
    public static final double DEFAULT_COMPARISON_THRESHOLD = 1e-10;

    /**
     * Constant defining minimum threshold.
     */
    public static final double MIN_THRESHOLD = 0.0;

    /**
     * Length of homogeneous coordinates array.
     */
    public static final int POINT3D_HOMOGENEOUS_COORDINATES_LENGTH = 4;

    /**
     * Length of inhomogeneous coordinates array.
     */
    public static final int POINT3D_INHOMOGENEOUS_COORDINATES_LENGTH = 3;

    /**
     * Default type of coordinates.
     */
    public static final CoordinatesType DEFAULT_COORDINATES_TYPE = CoordinatesType.HOMOGENEOUS_COORDINATES;

    /**
     * Constructor of this class.
     */
    protected Point3D() {
    }

    /**
     * Creates and returns an instance of any existing subclass of Point3D
     * specified in coordinatesType. The right size of the provided array is
     * also checked depending on the type of coordinates used.
     *
     * @param coordinatesType Type of coordinates used.
     * @param v               Array containing the coordinates of the 3D homogeneous or
     *                        inhomogeneous point.
     * @return Created Point3D.
     * @throws IllegalArgumentException Raised if the size of provided array is
     *                                  not valid.
     */
    public static Point3D create(final CoordinatesType coordinatesType, final double[] v) {
        if (coordinatesType == CoordinatesType.INHOMOGENEOUS_COORDINATES) {
            return new InhomogeneousPoint3D(v);
        } else {
            return new HomogeneousPoint3D(v);
        }
    }

    /**
     * Creates and returns an instance of any existing subclass of Point3D
     * depending on provided vector length. Size of provided vector is also
     * checked to ensure it has appropriate size to represent 3D points either
     * using inhomogeneous or homogeneous coordinates.
     *
     * @param v Array containing the coordinates of the 3D homogeneous or
     *          inhomogeneous 3D point.
     * @return Created Point3D.
     * @throws IllegalArgumentException Raised if the size of provided array
     *                                  is not valid.
     */
    public static Point3D create(final double[] v) {
        return create(DEFAULT_COORDINATES_TYPE, v);
    }

    /**
     * Creates and returns an instance of any existing subclass of Point3D
     * specified in coordinatesType.
     *
     * @param coordinatesType Type of coordinates used.
     * @return Created Point3D.
     */
    public static Point3D create(final CoordinatesType coordinatesType) {
        if (coordinatesType == CoordinatesType.INHOMOGENEOUS_COORDINATES) {
            return new InhomogeneousPoint3D();
        } else {
            return new HomogeneousPoint3D();
        }
    }

    /**
     * Creates and returns an instance of an existing subclass of Point3D
     * using DEFAULT_COORDINATES_TYPE.
     *
     * @return Create Point3D.
     */
    public static Point3D create() {
        return create(DEFAULT_COORDINATES_TYPE);
    }

    /**
     * Returns an array containing the coordinates of this Point3D.
     *
     * @return Array containing coordinates of this Point3D.
     */
    public abstract double[] asArray();

    /**
     * Uses provided array to store the coordinates of this Point3D
     *
     * @param array Array where coordinates will be stored.
     * @throws IllegalArgumentException Raised if length of array is not valid.
     */
    public abstract void asArray(final double[] array);

    /**
     * Sets the coordinates of a 3D point using an array containing its
     * coordinates.
     *
     * @param v Array containing the coordinates of the point.
     * @throws IllegalArgumentException Raised if provided array does not have
     *                                  a valid size.
     */
    public abstract void setCoordinates(final double[] v);

    /**
     * Sets coordinates of this instance using the coordinates of provided 3D
     * point.
     *
     * @param point Input point.
     */
    public abstract void setCoordinates(final Point3D point);

    /**
     * Returns X homogeneous coordinate of this 3D point.
     *
     * @return X homogeneous coordinate.
     */
    public abstract double getHomX();

    /**
     * Returns Y homogeneous coordinate of this 3D point.
     *
     * @return Y homogeneous coordinate.
     */
    public abstract double getHomY();

    /**
     * Returns Z homogeneous coordinate of this 3D point.
     *
     * @return Z homogeneous coordinate.
     */
    public abstract double getHomZ();

    /**
     * Returns W homogeneous coordinate of this 3D point.
     *
     * @return W homogeneous coordinate.
     */
    public abstract double getHomW();

    /**
     * Sets coordinates of this 3D point instance using provided homogeneous
     * coordinates.
     *
     * @param homX x homogeneous coordinate.
     * @param homY y homogeneous coordinate.
     * @param homZ z homogeneous coordinate.
     * @param homW w homogeneous coordinate.
     */
    public abstract void setHomogeneousCoordinates(
            final double homX, final double homY, final double homZ, final double homW);

    /**
     * Returns X inhomogeneous coordinate of this 3D point.
     *
     * @return X inhomogeneous coordinate.
     */
    public abstract double getInhomX();

    /**
     * Sets X inhomogeneous coordinate of this 3D point.
     *
     * @param inhomX X inhomogeneous coordinate.
     */
    public abstract void setInhomX(final double inhomX);

    /**
     * Returns Y inhomogeneous coordinate of this 3D point.
     *
     * @return Y inhomogeneous coordinate.
     */
    public abstract double getInhomY();

    /**
     * Sets Y inhomogeneous coordinate of this 3D point.
     *
     * @param inhomY Y inhomogeneous coordinate.
     */
    public abstract void setInhomY(final double inhomY);

    /**
     * Returns Z inhomogeneous coordinate of this 3D point.
     *
     * @return Z inhomogeneous coordinate.
     */
    public abstract double getInhomZ();

    /**
     * Sets Z inhomogeneous coordinate of this 3D point.
     *
     * @param inhomZ Z inhomogeneous coordinate.
     */
    public abstract void setInhomZ(final double inhomZ);

    /**
     * Sets coordinates of this 3D point instance using provided inhomogeneous
     * coordinates.
     *
     * @param inhomX x inhomogeneous coordinate.
     * @param inhomY y inhomogeneous coordinate.
     * @param inhomZ z inhomogeneous coordinate.
     */
    public abstract void setInhomogeneousCoordinates(
            final double inhomX, final double inhomY, final double inhomZ);

    /**
     * Checks if the 3D point described by this class equals the input Point3D
     * (using a comparison threshold).
     *
     * @param point     Point that will be compared to.
     * @param threshold threshold used to check that the difference of the
     *                  values is close to zero with an absolute error defined by threshold.
     * @return True if current point and input point are the same, false
     * otherwise.
     * @throws IllegalArgumentException Raised if threshold is negative.
     */
    public abstract boolean equals(final Point3D point, final double threshold);

    /**
     * Checks if the 3D point described by this class equals the input Point3D
     * (using DEFAULT_COMPARISON_THRESHOLD).
     *
     * @param point Point that will be compared to.
     * @return True if current point and input point are the same, false
     * otherwise.
     */
    public boolean equals(final Point3D point) {
        return equals(point, DEFAULT_COMPARISON_THRESHOLD);
    }

    /**
     * Checks if provided object equals current 3D point.
     *
     * @param obj Object to compare.
     * @return True if both objects are considered to be equal, false otherwise.
     */
    @Override
    public boolean equals(final Object obj) {
        if (!(obj instanceof Point3D point)) {
            return false;
        }
        if (obj == this) {
            return true;
        }

        return equals(point);
    }

    /**
     * Returns hash code value. This is only defined to keep the compiler happy.
     * This method must be overridden in subclasses of this class.
     *
     * @return Hash code.
     */
    @Override
    public abstract int hashCode();

    /**
     * Checks whether this Point3D is at infinity or not.
     *
     * @return True if the point is at infinity. False otherwise.
     */
    public abstract boolean isAtInfinity();

    /**
     * Returns the type of coordinates used to represent a Point3D.
     *
     * @return Type of coordinates of this 2d point.
     */
    public abstract CoordinatesType getType();

    /**
     * Method to normalize a 3D point. This only applies to homogeneous
     * 2d points, otherwise it has no effect.
     * This method is meant to be overridden.
     */
    public void normalize() {
    }

    /**
     * Returns boolean indicating whether this point has already been
     * normalized.
     * This method is meant to be overridden. By default, it will always return
     * true, to indicate that no further normalization is possible.
     *
     * @return True if normalized, false otherwise.
     */
    public boolean isNormalized() {
        return true;
    }

    /**
     * Returns number of dimensions of this point implementation.
     *
     * @return number of dimensions.
     */
    @Override
    public int getDimensions() {
        return POINT3D_INHOMOGENEOUS_COORDINATES_LENGTH;
    }

    /**
     * Gets value of inhomogeneous coordinate for provided dimension.
     *
     * @param dim dimension to retrieve coordinate for (i.e. 0 means x, 1 means y, 2 means z, etc).
     * @return value of inhomogeneous coordinate.
     * @throws IllegalArgumentException if provided dimension value is negative or exceeds number of dimensions.
     */
    @Override
    public double getInhomogeneousCoordinate(final int dim) {
        if (dim < 0 || dim >= getDimensions()) {
            throw new IllegalArgumentException();
        }

        return switch (dim) {
            case 0 -> getInhomX();
            case 1 -> getInhomY();
            default -> getInhomZ();
        };
    }

    /**
     * Sets value of inhomogeneous coordinate for provided dimension.
     *
     * @param dim   dimension to set coordinate for (i.e. 0 means x, 1 means y, 2 means z, etc.).
     * @param value value to be set.
     * @throws IllegalArgumentException if provided dimension value is negative or exceeds number of dimensions.
     */
    @Override
    public void setInhomogeneousCoordinate(final int dim, final double value) {
        switch (dim) {
            case 0:
                setInhomX(value);
                break;
            case 1:
                setInhomY(value);
                break;
            case 2:
                setInhomZ(value);
                break;
            default:
                throw new IllegalArgumentException();
        }
    }

    /**
     * Returns Euclidean distance between this point and provided point.
     *
     * @param point Point to compare.
     * @return Euclidean distance between this point and provided point.
     */
    @Override
    public double distanceTo(final Point3D point) {
        return Math.sqrt(sqrDistanceTo(point));
    }

    /**
     * Returns squared Euclidean distance between this point and provided point.
     *
     * @param point point to compare.
     * @return Euclidean distance between this point and provided point.
     */
    @Override
    public double sqrDistanceTo(final Point3D point) {
        final var diffX = getInhomX() - point.getInhomX();
        final var diffY = getInhomY() - point.getInhomY();
        final var diffZ = getInhomZ() - point.getInhomZ();

        return diffX * diffX + diffY * diffY + diffZ * diffZ;
    }

    /**
     * Computes the dot product between the homogeneous coordinates x, y, z, w
     * of this point and the ones of provided point.
     *
     * @param point point to compute dot product with.
     * @return dot product value.
     */
    public double dotProduct(final Point3D point) {
        final var thisHomX = getHomX();
        final var thisHomY = getHomY();
        final var thisHomZ = getHomZ();
        final var thisHomW = getHomW();
        final var otherHomX = point.getHomX();
        final var otherHomY = point.getHomY();
        final var otherHomZ = point.getHomZ();
        final var otherHomW = point.getHomW();

        final var thisNormSqr = thisHomX * thisHomX + thisHomY * thisHomY + thisHomZ * thisHomZ + thisHomW * thisHomW;
        final var otherNormSqr = otherHomX * otherHomX + otherHomY * otherHomY + otherHomZ * otherHomZ
                + otherHomW * otherHomW;
        final var denom = Math.sqrt(thisNormSqr * otherNormSqr);
        final var num = thisHomX * otherHomX + thisHomY * otherHomY + thisHomZ * otherHomZ + thisHomW * otherHomW;

        return num / denom;
    }

    /**
     * Returns true if this point is between points point1 and point2, in other
     * words, is inside the segment formed by those 2 points.
     *
     * @param point1 Point 1.
     * @param point2 Point 2.
     * @return True if point is between point1 and point2, false otherwise.
     */
    public boolean isBetween(final Point3D point1, final Point3D point2) {
        return isBetween(point1, point2, DEFAULT_COMPARISON_THRESHOLD);
    }

    /**
     * Returns true if this point is between points point1 and point2, in other
     * words, is inside the segment formed by those 2 points.
     *
     * @param point1    Point 1.
     * @param point2    Point 2.
     * @param threshold Threshold to determine if point is between.
     * @return True if point is between point1 and point2, false otherwise.
     */
    public boolean isBetween(final Point3D point1, final Point3D point2, final double threshold) {
        if (threshold < MIN_THRESHOLD) {
            throw new IllegalArgumentException();
        }
        // If this point is between point1 and point2 then,
        // dist(point1,this) + dist(point2, this) == dist(point1,point2) except
        // for some small difference due to machine precision
        return Math.abs(distanceTo(point1) + distanceTo(point2) - point1.distanceTo(point2)) <= threshold;
    }

    /**
     * Computes the centroid of provided collection of points by computing the
     * mean of their inhomogeneous coordinates.
     *
     * @param points collection of points to compute centroid from.
     * @param result instance where computed centroid will be stored.
     */
    public static void centroid(final Collection<Point3D> points, final Point3D result) {
        var x = 0.0;
        var y = 0.0;
        var z = 0.0;
        if (points != null) {
            final var n = points.size();
            for (final var point : points) {
                x += point.getInhomX();
                y += point.getInhomY();
                z += point.getInhomZ();
            }

            x /= n;
            y /= n;
            z /= n;
        }
        result.setInhomogeneousCoordinates(x, y, z);
    }

    /**
     * Computes the centroid of provided collection of points by computing the
     * mean of their inhomogeneous coordinates.
     *
     * @param points collection of points to compute centroid from.
     * @return computed centroid.
     */
    public static Point3D centroid(final Collection<Point3D> points) {
        final var result = Point3D.create();
        centroid(points, result);
        return result;
    }
}