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}