    001/* ===========================================================
    002 * Orson Charts : a 3D chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
    006 *
    007 * https://github.com/jfree/orson-charts
    008 *
    009 * This program is free software: you can redistribute it and/or modify
    010 * it under the terms of the GNU General Public License as published by
    011 * the Free Software Foundation, either version 3 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This program is distributed in the hope that it will be useful,
    015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
    016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    017 * GNU General Public License for more details.
    018 *
    019 * You should have received a copy of the GNU General Public License
    020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    021 *
    022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    023 * Other names may be trademarks of their respective owners.]
    024 *
    025 * If you do not wish to be bound by the terms of the GPL, an alternative
    026 * commercial license can be purchased.  For details, please see visit the
    027 * Orson Charts home page:
    028 *
    029 * http://www.object-refinery.com/orsoncharts/index.html
    030 *
    031 */
    032
    033package org.jfree.chart3d.graphics3d;
    034
    035import java.awt.Color;
    036import java.awt.Dimension;
    037import java.awt.geom.Dimension2D;
    038import java.awt.geom.Point2D;
    039import java.io.Serializable;
    040import org.jfree.chart3d.graphics3d.internal.Utils2D;
    041import org.jfree.chart3d.graphics3d.internal.Utils3D;
    042
    043/**
    044 * Specifies the location and orientation of the view point in 3D space.
    045 * Assumes the eye looks towards the origin in world coordinates.
    046 * <br><br>
    047 * There are four basic operations to move the view point:
    048 * <ul>
    049 * <li>{@link #panLeftRight(double)} - rotates around the scene horizontally
    050 *     from the perspective of the viewer;</li>
    051 * <li>{@link #moveUpDown(double)} - rotates around the scene vertically from
    052 *     the perspective of the viewer;</li>
    053 * <li>{@link #roll(double)} - maintains the same viewing location but rolls
    054 *     by the specified angle (like tilting a camera);</li>
    055 * <li>{@link #setRho(double)} - sets the distance of the view location from
    056 *     the center of the 3D scene (zoom in and out).</li>
    057 * </ul>
    058 * <br><br>
    059 * NOTE: This class is serializable, but the serialization format is subject
    060 * to change in future releases and should not be relied upon for persisting
    061 * instances of this class.
    062 */
    063@SuppressWarnings("serial")
    064public class ViewPoint3D implements Serializable {
    065
    066    /**
    067     * Creates and returns a view point for looking at a chart from the
    068     * front and above.
    069     *
    070     * @param rho  the distance.
    071     *
    072     * @return A view point.
    073     */
    074    public static ViewPoint3D createAboveViewPoint(double rho) {
    075        return new ViewPoint3D(-Math.PI / 2, 9 * Math.PI / 8, rho, 0);
    076    }
    077
    078    /**
    079     * Creates and returns a view point for looking at a chart from the
    080     * front and above and to the left.
    081     *
    082     * @param rho  the distance.
    083     *
    084     * @return A view point.
    085     */
    086    public static ViewPoint3D createAboveLeftViewPoint(double rho) {
    087        ViewPoint3D vp = createAboveViewPoint(rho);
    088        vp.panLeftRight(-Math.PI / 6);
    089        return vp;
    090    }
    091
    092    /**
    093     * Creates and returns a view point for looking at a chart from the
    094     * front and above and to the right.
    095     *
    096     * @param rho  the distance.
    097     *
    098     * @return A view point.
    099     */
    100    public static ViewPoint3D createAboveRightViewPoint(double rho) {
    101        ViewPoint3D vp = createAboveViewPoint(rho);
    102        vp.panLeftRight(Math.PI / 6);
    103        return vp;
    104    }
    105
    106    /** The rotation of the viewing point from the x-axis around the z-axis. */
    107    private double theta;
    108
    109    /** The rotation (up and down) of the viewing point. */
    110    private double phi;
    111
    112    /** The distance of the viewing point from the origin. */
    113    private double rho;
    114
    115    /** Transformation matrix elements. */
    116    private double v11, v12, v13, v21, v22, v23, v32, v33, v43;
    117
    118    /**
    119     * A point 1/4 turn "upwards" on the sphere, to define the camera
    120     * orientation.
    121     */
    122    private Point3D up;
    123
    124    /** Applies the rotation for the orientation of the view. */
    125    private Rotate3D rotation;
    126
    127    /** A workspace for calling the Rotate3D class. */
    128    private double[] workspace;
    129
    130    /**
    131     * Creates a new viewing point.
    132     *
    133     * @param theta  the rotation of the viewing point from the x-axis around
    134     *     the z-axis (in radians)
    135     * @param phi  the rotation of the viewing point up and down (from the
    136     *     XZ plane, in radians)
    137     * @param rho  the distance of the viewing point from the origin.
    138     * @param orientation  the angle of rotation.
    139     */
    140    public ViewPoint3D(double theta, double phi, double rho,
    141            double orientation) {
    142        this.theta = theta;
    143        this.phi = phi;
    144        this.rho = rho;
    145        updateMatrixElements();
    146        this.rotation = new Rotate3D(Point3D.ORIGIN, Point3D.UNIT_Z,
    147                orientation);
    148        this.up = this.rotation.applyRotation(Point3D.createPoint3D(this.theta,
    149                this.phi - Math.PI / 2, this.rho));
    150        this.workspace = new double[3];
    151    }
    152
    153    /**
    154     * Creates a new instance using the specified point and orientation.
    155     *
    156     * @param p  the viewing point.
    157     * @param orientation  the orientation.
    158     */
    159    public ViewPoint3D(Point3D p, double orientation) {
    160        this.rho = (float) Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
    161        if (Math.sqrt(p.x * p.x + p.y * p.y) > 0.000001) {
    162            this.theta = (float) Math.atan2(p.y, p.x);
    163        }
    164        this.phi = (float) Math.acos(p.z / this.rho);
    165        updateMatrixElements();
    166        this.rotation = new Rotate3D( Point3D.ORIGIN, Point3D.UNIT_Z,
    167                orientation);
    168        this.up = this.rotation.applyRotation(Point3D.createPoint3D(this.theta,
    169                this.phi - Math.PI / 2, this.rho));
    170        this.workspace = new double[3];
    171    }
    172
    173    /**
    174     * Creates a new instance that is an exact copy of the supplied viewpoint.
    175     *
    176     * @param vp  the view point ({@code null} not permitted).
    177     *
    178     * @since 1.6.1
    179     */
    180    public ViewPoint3D(ViewPoint3D vp) {
    181        this.theta = vp.theta;
    182        this.phi = vp.phi;
    183        this.rho = vp.rho;
    184        updateMatrixElements();
    185        this.rotation = new Rotate3D(Point3D.ORIGIN, Point3D.UNIT_Z,
    186                vp.rotation.angle);
    187        this.up = vp.up;
    188        this.workspace = new double[3];
    189    }
    190
    191   /**
    192     * Returns the angle of rotation from the x-axis about the z-axis,
    193     * in radians.  This attribute is set via the constructor and updated
    194     * via the {@link #panLeftRight(double)} and {@link #moveUpDown(double)}
    195     * methods - there is no setter method, you cannot update it directly.
    196     *
    197     * @return The angle (in radians).
    198     */
    199    public final double getTheta() {
    200        return this.theta;
    201    }
    202
    203    /**
    204     * Returns the angle of the viewing point down from the z-axis.  This
    205     * attribute is set via the constructor and updated via the
    206     * {@link #panLeftRight(double)} and {@link #moveUpDown(double)} methods
    207     * - there is no setter method, you cannot update it directly.
    208     *
    209     * @return The angle of the viewing point down from the z-axis.
    210     *     (in radians).
    211     */
    212    public final double getPhi() {
    213        return this.phi;
    214    }
    215
    216    /**
    217     * Returns the distance of the viewing point from the origin.
    218     *
    219     * @return The distance of the viewing point from the origin.
    220     *
    221     * @see #setRho(double)
    222     */
    223    public final double getRho() {
    224        return this.rho;
    225    }
    226
    227    /**
    228     * Sets the distance of the viewing point from the origin.
    229     *
    230     * @param rho  the new distance.
    231     */
    232    public void setRho(double rho) {
    233        this.rho = rho;
    234        this.up = Point3D.createPoint3D(this.up.getTheta(), this.up.getPhi(),
    235                rho);
    236        updateMatrixElements();
    237    }
    238
    239    /**
    240     * Returns the x-coordinate of the viewing point.  This value is
    241     * calculated from the spherical coordinates.
    242     *
    243     * @return The x-coordinate of the viewing point.
    244     */
    245    public final double getX() {
    246        return this.rho * Math.sin(this.phi) * Math.cos(this.theta);
    247    }
    248
    249    /**
    250     * Returns the y-coordinate of the viewing point.  This value is
    251     * calculated from the spherical coordinates.
    252     *
    253     * @return The y-coordinate of the viewing point.
    254     */
    255    public final double getY() {
    256        return this.rho * Math.sin(this.phi) * Math.sin(this.theta);
    257    }
    258
    259    /**
    260     * Returns the z-coordinate of the viewing point.  This value is
    261     * calculated from the spherical coordinates.
    262     *
    263     * @return The z-coordinate of the viewing point.
    264     */
    265    public final double getZ() {
    266        return this.rho * Math.cos(this.phi);
    267    }
    268
    269    /**
    270     * Returns the location of the view point.  Note that a new instance of
    271     * {@code Point3D} is created each time this method is called.
    272     *
    273     * @return The viewing point (never {@code null}).
    274     */
    275    public final Point3D getPoint() {
    276        return new Point3D(getX(), getY(), getZ());
    277    }
    278
    279    /**
    280     * Returns the roll angle (orientation) for the view point.  This is
    281     * calculated by reference to second point on the sphere that is a
    282     * quarter turn from the view point location (this second point defines
    283     * the "up" direction for the view).
    284     *
    285     * @return The roll angle (in radians).
    286     */
    287    public double calcRollAngle() {
    288        Point3D vp = getPoint();
    289        Point3D n1 = Utils3D.normal(vp, this.up, Point3D.ORIGIN);
    290        Point3D screenup = Point3D.createPoint3D(this.theta,
    291               this.phi - (Math.PI / 2), this.rho);
    292        Point3D n2 = Utils3D.normal(vp, screenup, Point3D.ORIGIN);
    293        double angle = Utils3D.angle(n1, n2);
    294        if (Utils3D.scalarprod(n1, screenup) >= 0.0) {
    295            return angle;
    296        } else {
    297            return -angle;
    298        }
    299    }
    300
    301    /**
    302     * Moves the viewing point left or right around the 3D scene.
    303     *
    304     * @param delta  the angle (in radians).
    305     */
    306    public void panLeftRight(double delta) {
    307        Point3D v = getVerticalRotationAxis();
    308        Rotate3D r = new Rotate3D(Point3D.ORIGIN, v, delta);
    309        Point3D p = r.applyRotation(getX(), getY(), getZ());
    310        this.theta = p.getTheta();
    311        this.phi = p.getPhi();
    312        updateMatrixElements();
    313        this.rotation.setAngle(calcRollAngle());
    314    }
    315
    316    /**
    317     * Moves the viewing point up or down on the viewing sphere.
    318     *
    319     * @param delta  the angle delta (in radians).
    320     */
    321    public void moveUpDown(double delta) {
    322        Point3D v = getHorizontalRotationAxis();
    323        Rotate3D r = new Rotate3D(Point3D.ORIGIN, v, delta);
    324        Point3D p = r.applyRotation(getX(), getY(), getZ());
    325        this.up = r.applyRotation(this.up);
    326        this.theta = p.getTheta();
    327        this.phi = p.getPhi();
    328        updateMatrixElements();
    329        this.rotation.setAngle(calcRollAngle());
    330    }
    331
    332    /**
    333     * Rolls the view while leaving the location of the view point unchanged.
    334     *
    335     * @param delta  the angle (in radians).
    336     */
    337    public void roll(double delta) {
    338        // we rotate the "up" point around the sphere by delta radians
    339        Rotate3D r = new Rotate3D(getPoint(), Point3D.ORIGIN, delta);
    340        this.up = r.applyRotation(this.up);
    341        this.rotation.setAngle(calcRollAngle());
    342    }
    343
    344    /**
    345     * Converts a point in world coordinates to a point in eye coordinates.
    346     *
    347     * @param p  the point ({@code null} not permitted).
    348     *
    349     * @return The point in eye coordinates.
    350     */
    351    public Point3D worldToEye(Point3D p) {
    352        double x = this.v11 * p.x + this.v21 * p.y;
    353        double y = this.v12 * p.x + this.v22 * p.y + this.v32 * p.z;
    354        double z = this.v13 * p.x + this.v23 * p.y + this.v33 * p.z + this.v43;
    355        double[] rotated = this.rotation.applyRotation(x, y, z, this.workspace);
    356        return new Point3D(rotated[0], rotated[1], rotated[2]);
    357    }
    358
    359    /**
    360     * Calculates and returns the screen coordinates for the specified point
    361     * in (world) 3D space.
    362     *
    363     * @param p  the point.
    364     * @param d  the projection distance.
    365     *
    366     * @return The screen coordinate.
    367     */
    368    public Point2D worldToScreen(Point3D p, double d) {
    369        double x = this.v11 * p.x + this.v21 * p.y;
    370        double y = this.v12 * p.x + this.v22 * p.y + this.v32 * p.z;
    371        double z = this.v13 * p.x + this.v23 * p.y + this.v33 * p.z + this.v43;
    372        double[] rotated = this.rotation.applyRotation(x, y, z, this.workspace);
    373        return new Point2D.Double(-d * rotated[0] / rotated[2],
    374                -d * rotated[1] / rotated[2]);
    375    }
    376
    377    /**
    378     * Calculate the distance that would render a box of the given dimensions
    379     * within a screen area of the specified size.
    380     *
    381     * @param target  the target dimension ({@code null} not permitted).
    382     * @param dim3D  the dimensions of the 3D content ({@code null} not
    383     *     permitted).
    384     * @param projDist  the projection distance.
    385     *
    386     * @return The optimal viewing distance.
    387     */
    388    public float optimalDistance(Dimension2D target, Dimension3D dim3D,
    389            double projDist) {
    390
    391        ViewPoint3D vp = new ViewPoint3D(this.theta, this.phi, this.rho,
    392                calcRollAngle());
    393        float near = (float) dim3D.getDiagonalLength();
    394        float far = near * 40;
    395
    396        World w = new World();
    397        double ww = dim3D.getWidth();
    398        double hh = dim3D.getHeight();
    399        double dd = dim3D.getDepth();
    400        w.add(Object3D.createBox(0, ww, 0, hh, 0, dd, Color.RED));
    401
    402        while (true) {
    403            vp.setRho(near);
    404            Point2D[] nearpts = w.calculateProjectedPoints(vp, projDist);
    405            Dimension neardim = Utils2D.findDimension(nearpts);
    406            double nearcover = coverage(neardim, target);
    407            vp.setRho(far);
    408            Point2D[] farpts = w.calculateProjectedPoints(vp, projDist);
    409            Dimension fardim = Utils2D.findDimension(farpts);
    410            double farcover = coverage(fardim, target);
    411            if (nearcover <= 1.0) {
    412                return near;
    413            }
    414            if (farcover >= 1.0) {
    415                return far;
    416            }
    417            // bisect near and far until we get close enough to the specified
    418            // dimension
    419            float mid = (near + far) / 2.0f;
    420            vp.setRho(mid);
    421            Point2D[] midpts = w.calculateProjectedPoints(vp, projDist);
    422            Dimension middim = Utils2D.findDimension(midpts);
    423            double midcover = coverage(middim, target);
    424            if (midcover >= 1.0) {
    425                near = mid;
    426            } else {
    427                far = mid;
    428            }
    429        }
    430    }
    431
    432    private double coverage(Dimension2D d, Dimension2D target) {
    433        double wpercent = d.getWidth() / target.getWidth();
    434        double hpercent = d.getHeight() / target.getHeight();
    435        if (wpercent <= 1.0 && hpercent <= 1.0) {
    436            return Math.max(wpercent, hpercent);
    437        } else {
    438            if (wpercent >= 1.0) {
    439                if (hpercent >= 1.0) {
    440                    return Math.max(wpercent, hpercent);
    441                } else {
    442                    return wpercent;
    443                }
    444            } else {
    445                return hpercent;  // don't think it will matter
    446            }
    447        }
    448    }
    449
    450    /**
    451     * Updates the matrix elements.
    452     */
    453    private void updateMatrixElements() {
    454        float cosTheta = (float) Math.cos(this.theta);
    455        float sinTheta = (float) Math.sin(this.theta);
    456        float cosPhi = (float) Math.cos(this.phi);
    457        float sinPhi = (float) Math.sin(this.phi);
    458        this.v11 = -sinTheta;
    459        this.v12 = -cosPhi * cosTheta;
    460        this.v13 = sinPhi * cosTheta;
    461        this.v21 = cosTheta;
    462        this.v22 = -cosPhi * sinTheta;
    463        this.v23 = sinPhi * sinTheta;
    464        this.v32 = sinPhi;
    465        this.v33 = cosPhi;
    466        this.v43 = -this.rho;
    467    }
    468
    469
    470    /**
    471     * Returns the vector that points "up" in relation to the orientation of
    472     * the view point.  This vector can be used to rotate the viewing point
    473     * around the 3D scene (pan left / right).
    474     *
    475     * @return The vector (never {@code null}).
    476     */
    477    public Point3D getVerticalRotationAxis() {
    478        return this.up;
    479    }
    480
    481    /**
    482     * Returns a vector at right angles to the viewing direction and the "up"
    483     * vector (this axis can be used to rotate forward and backwards).
    484     *
    485     * @return A vector (never {@code null}).
    486     */
    487    public Point3D getHorizontalRotationAxis() {
    488        return Utils3D.normal(getPoint(), this.up, Point3D.ORIGIN);
    489    }
    490
    491    /**
    492     * Returns a string representation of this instance, primarily for
    493     * debugging purposes.
    494     *
    495     * @return A string.
    496     */
    497    @Override
    498    public String toString() {
    499        return "[theta=" + this.theta + ", phi=" + this.phi + ", rho="
    500                + this.rho + "]";
    501    }
    502
    503    /**
    504     * Tests this view point for equality with an arbitrary object.
    505     *
    506     * @param obj  the object ({@code null} permitted).
    507     *
    508     * @return A boolean.
    509     */
    510    @Override
    511    public boolean equals(Object obj) {
    512        if (obj == this) {
    513            return true;
    514        }
    515        if (!(obj instanceof ViewPoint3D)) {
    516            return false;
    517        }
    518        ViewPoint3D that = (ViewPoint3D) obj;
    519        if (this.theta != that.theta) {
    520            return false;
    521        }
    522        if (this.phi != that.phi) {
    523            return false;
    524        }
    525        if (this.rho != that.rho) {
    526            return false;
    527        }
    528        if (!this.up.equals(that.up)) {
    529            return false;
    530        }
    531        return true;
    532    }
    533
    534}