001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.util.imagery;
003
004import org.openstreetmap.josm.tools.Utils;
005
006/**
007 * A basic 3D vector class (immutable)
008 * @author Taylor Smock (documentation, spherical conversions)
009 * @since 18246
010 */
011public final class Vector3D {
012    /**
013     * This determines how arguments are used in {@link Vector3D#Vector3D(VectorType, double, double, double)}.
014     */
015    public enum VectorType {
016        /** Standard cartesian coordinates (x, y, z) */
017        XYZ,
018        /** Physics (radial distance, polar angle, azimuthal angle) */
019        RPA,
020        /** Mathematics (radial distance, azimuthal angle, polar angle) */
021        RAP
022    }
023
024    /** A non-null default vector */
025    public static final Vector3D DEFAULT_VECTOR_3D = new Vector3D(0, 0, 1);
026
027    private final double x;
028    private final double y;
029    private final double z;
030    /* The following are all lazily calculated, but should always be the same */
031    /** The radius r */
032    private volatile double radialDistance = Double.NaN;
033    /** The polar angle theta (inclination) */
034    private volatile double polarAngle = Double.NaN;
035    /** Cosine of polar angle (angle from Z axis, AKA straight up) */
036    private volatile double polarAngleCos = Double.NaN;
037    /** Sine of polar angle (angle from Z axis, AKA straight up) */
038    private volatile double polarAngleSin = Double.NaN;
039    /** The azimuthal angle phi */
040    private volatile double azimuthalAngle = Double.NaN;
041    /** Cosine of azimuthal angle (angle from X axis) */
042    private volatile double azimuthalAngleCos = Double.NaN;
043    /** Sine of azimuthal angle (angle from X axis) */
044    private volatile double azimuthalAngleSin = Double.NaN;
045
046    /**
047     * Create a new Vector3D object using the XYZ coordinate system
048     *
049     * @param x The x coordinate
050     * @param y The y coordinate
051     * @param z The z coordinate
052     */
053    public Vector3D(double x, double y, double z) {
054        this(VectorType.XYZ, x, y, z);
055    }
056
057    /**
058     * Create a new Vector3D object. See ordering in {@link VectorType}.
059     *
060     * @param first The first coordinate
061     * @param second The second coordinate
062     * @param third The third coordinate
063     * @param vectorType The coordinate type (determines how the other variables are treated)
064     */
065    public Vector3D(VectorType vectorType, double first, double second, double third) {
066        if (vectorType == VectorType.XYZ) {
067            this.x = first;
068            this.y = second;
069            this.z = third;
070        } else {
071            this.radialDistance = first;
072            if (vectorType == VectorType.RPA) {
073                this.azimuthalAngle = third;
074                this.polarAngle = second;
075            } else {
076                this.azimuthalAngle = second;
077                this.polarAngle = third;
078            }
079            // Since we have to run the calculations anyway, ensure they are cached.
080            this.x = this.radialDistance * this.getAzimuthalAngleCos() * this.getPolarAngleSin();
081            this.y = this.radialDistance * this.getAzimuthalAngleSin() * this.getPolarAngleSin();
082            this.z = this.radialDistance * this.getPolarAngleCos();
083        }
084    }
085
086    /**
087     * Get the x coordinate
088     *
089     * @return The x coordinate
090     */
091    public double getX() {
092        return x;
093    }
094
095    /**
096     * Get the y coordinate
097     *
098     * @return The y coordinate
099     */
100    public double getY() {
101        return y;
102    }
103
104    /**
105     * Get the z coordinate
106     *
107     * @return The z coordinate
108     */
109    public double getZ() {
110        return z;
111    }
112
113    /**
114     * Get the radius
115     *
116     * @return The radius
117     */
118    public double getRadialDistance() {
119        if (Double.isNaN(this.radialDistance)) {
120            this.radialDistance = Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
121        }
122        return this.radialDistance;
123    }
124
125    /**
126     * Get the polar angle (inclination)
127     *
128     * @return The polar angle
129     */
130    public double getPolarAngle() {
131        if (Double.isNaN(this.polarAngle)) {
132            // This was Math.atan(x, z) in the Mapillary plugin
133            // This should be Math.atan(y, z)
134            this.polarAngle = Math.atan2(this.x, this.z);
135        }
136        return this.polarAngle;
137    }
138
139    /**
140     * Get the polar angle cossine (inclination)
141     *
142     * @return The polar angle cosine
143     */
144    public double getPolarAngleCos() {
145        if (Double.isNaN(this.polarAngleCos)) {
146            this.polarAngleCos = Math.cos(this.getPolarAngle());
147        }
148        return this.polarAngleCos;
149    }
150
151    /**
152     * Get the polar angle sine (inclination)
153     *
154     * @return The polar angle sine
155     */
156    public double getPolarAngleSin() {
157        if (Double.isNaN(this.polarAngleSin)) {
158            this.polarAngleSin = Math.sin(this.getPolarAngle());
159        }
160        return this.polarAngleSin;
161    }
162
163    /**
164     * Get the azimuthal angle
165     *
166     * @return The azimuthal angle
167     */
168    public double getAzimuthalAngle() {
169        if (Double.isNaN(this.azimuthalAngle)) {
170            if (Double.isNaN(this.radialDistance)) {
171                // Force calculation
172                this.getRadialDistance();
173            }
174            // Avoid issues where x, y, and z are 0
175            if (this.radialDistance == 0) {
176                this.azimuthalAngle = 0;
177            } else {
178                // This was Math.acos(y / radialDistance) in the Mapillary plugin
179                // This should be Math.acos(z / radialDistance)
180                this.azimuthalAngle = Math.acos(this.y / this.radialDistance);
181            }
182        }
183        return this.azimuthalAngle;
184    }
185
186    /**
187     * Get the azimuthal angle cosine
188     *
189     * @return The azimuthal angle cosine
190     */
191    public double getAzimuthalAngleCos() {
192        if (Double.isNaN(this.azimuthalAngleCos)) {
193            this.azimuthalAngleCos = Math.cos(this.getAzimuthalAngle());
194        }
195        return this.azimuthalAngleCos;
196    }
197
198    /**
199     * Get the azimuthal angle sine
200     *
201     * @return The azimuthal angle sine
202     */
203    public double getAzimuthalAngleSin() {
204        if (Double.isNaN(this.azimuthalAngleSin)) {
205            this.azimuthalAngleSin = Math.sin(this.getAzimuthalAngle());
206        }
207        return this.azimuthalAngleSin;
208    }
209
210    /**
211     * Normalize the vector
212     *
213     * @return A normalized vector
214     */
215    public Vector3D normalize() {
216        final double length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2));
217        final double newX;
218        final double newY;
219        final double newZ;
220        if (length == 0 || Double.isNaN(length)) {
221            newX = 0;
222            newY = 0;
223            newZ = 0;
224        } else {
225            newX = x / length;
226            newY = y / length;
227            newZ = z / length;
228        }
229        return new Vector3D(newX, newY, newZ);
230    }
231
232    @Override
233    public int hashCode() {
234        return Double.hashCode(this.x) + 31 * Double.hashCode(this.y) + 31 * 31 * Double.hashCode(this.z);
235    }
236
237    @Override
238    public boolean equals(Object o) {
239        if (o instanceof Vector3D) {
240            Vector3D other = (Vector3D) o;
241            return Utils.equalsEpsilon(this.x, other.x) && Utils.equalsEpsilon(this.y, other.y) && Utils.equalsEpsilon(this.z, other.z);
242        }
243        return false;
244    }
245
246    @Override
247    public String toString() {
248        return "[x=" + this.x + ", y=" + this.y + ", z=" + this.z + ", r=" + this.radialDistance + ", inclination="
249            + this.polarAngle + ", azimuthal=" + this.azimuthalAngle + "]";
250    }
251}