Direction3d.java

package org.djutils.draw;

import java.util.Objects;

import org.djutils.exceptions.Throw;

/**
 * Class encoding a direction in 3d space. It combines dirY (similar to tilt; measured as an angle from the positive
 * z-direction) and dirZ (similar to pan; measured as an angle from the positive x-direction).
 * <p>
 * Copyright (c) 2023-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
 * distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 */
public class Direction3d
{
    /** Rotation around y-axis. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    public final double dirY;

    /** Rotation around z-axis. */
    @SuppressWarnings("checkstyle:visibilitymodifier")
    public final double dirZ;

    /**
     * Construct a Direction3d.
     * @param dirY the dirY component for the new Direction3d
     * @param dirZ the dirZ component for the new Direction3d
     * @throws ArithmeticException when <code>dirY</code>, or <code>dirZ</code> is <code>NaN</code>
     * @throws IllegalArgumentException when <code>dirY</code>, or <code>dirZ</code> is infinite
     */
    public Direction3d(final double dirY, final double dirZ)
    {
        Throw.whenNaN(dirY, "dirY");
        Throw.whenNaN(dirZ, "dirZ");
        Throw.when((!Double.isFinite(dirY)) || (!Double.isFinite(dirZ)), IllegalArgumentException.class,
                "dirY and dirZ must be finite");
        this.dirY = dirY;
        this.dirZ = dirZ;
    }

    /**
     * Retrieve the dirY component of this Direction3d.
     * @return the <code>dirY</code> component of this <code>Direction3d</code>
     */
    public double getDirY()
    {
        return this.dirY;
    }

    /**
     * Retrieve the dirZ component of this Direction3d.
     * @return the <code>dirZ</code> component of this <code>Direction3d</code>
     */
    public double getDirZ()
    {
        return this.dirZ;
    }

    /**
     * Determine the angle between this Direction3d and another Direction3d. Liberally based on
     * https://www.cuemath.com/geometry/angle-between-vectors/
     * @param otherDirection the other Direction3d
     * @return double the angle in Radians
     * @throws NullPointerException when <code>otherDirection</code> is <code>null</code>
     */
    public double directionDifference(final Direction3d otherDirection)
    {
        double sinDirY = Math.sin(this.dirY);
        double uX = Math.cos(this.dirZ) * sinDirY;
        double uY = Math.sin(this.dirZ) * sinDirY;
        double uZ = Math.cos(this.dirY);
        double otherSinDirY = Math.sin(otherDirection.dirY);
        double oX = Math.cos(otherDirection.dirZ) * otherSinDirY;
        double oY = Math.sin(otherDirection.dirZ) * otherSinDirY;
        double oZ = Math.cos(otherDirection.dirY);
        double cosine = uX * oX + uY * oY + uZ * oZ;
        if (Math.abs(cosine) > 1.0 && Math.abs(cosine) < 1.0 + 10 * Math.ulp(1.0))
        {
            cosine = Math.signum(cosine); // Fix rounding error
        }
        return (Math.acos(cosine));
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(this.dirY, this.dirZ);
    }

    @Override
    @SuppressWarnings("checkstyle:needbraces")
    public boolean equals(final Object obj)
    {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Direction3d other = (Direction3d) obj;
        return Double.doubleToLongBits(this.dirY) == Double.doubleToLongBits(other.dirY)
                && Double.doubleToLongBits(this.dirZ) == Double.doubleToLongBits(other.dirZ);
    }

    @Override
    public String toString()
    {
        return "Direction3d [dirY=" + this.dirY + ", dirZ=" + this.dirZ + "]";
    }

}