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