    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.swing;
    034
    035import java.awt.BorderLayout;
    036import java.awt.Dimension;
    037import java.awt.Graphics;
    038import java.awt.Graphics2D;
    039import java.awt.Insets;
    040import java.awt.Point;
    041import java.awt.Rectangle;
    042import java.awt.geom.AffineTransform;
    043import java.awt.event.MouseEvent;
    044import java.awt.event.MouseListener;
    045import java.awt.event.MouseMotionListener;
    046import java.awt.event.MouseWheelEvent;
    047import java.awt.event.MouseWheelListener;
    048import java.awt.geom.Dimension2D;
    049import java.io.File;
    050
    051import javax.swing.JPanel;
    052import javax.swing.ToolTipManager;
    053
    054import com.orsoncharts.util.ArgChecks;
    055import com.orsoncharts.graphics3d.Drawable3D;
    056import com.orsoncharts.graphics3d.Offset2D;
    057import com.orsoncharts.graphics3d.ViewPoint3D;
    058import com.orsoncharts.graphics3d.Dimension3D;
    059import com.orsoncharts.graphics3d.ExportUtils;
    060import com.orsoncharts.graphics3d.RenderingInfo;
    061
    062/**
    063 * A panel that displays a set of 3D objects from a particular viewing point.
    064 * The view point is maintained by the {@link Drawable3D} but the panel
    065 * provides convenience methods to get/set it.
    066 * <br><br>
    067 * NOTE: This class is serializable, but the serialization format is subject
    068 * to change in future releases and should not be relied upon for persisting
    069 * instances of this class.
    070 */
    071@SuppressWarnings("serial")
    072public class Panel3D extends JPanel implements MouseListener,
    073        MouseMotionListener, MouseWheelListener {
    074
    075    /**
    076     * The object that is displayed in the panel.
    077     */
    078    private Drawable3D drawable;
    079
    080    /**
    081     * The minimum viewing distance (zooming in will not go closer than this).
    082     */
    083    private double minViewingDistance;
    084
    085    private double maxViewingDistanceMultiplier;
    086
    087    /**
    088     * The margin to leave around the edges of the chart when zooming to fit.
    089     * This is expressed as a percentage (0.25 = 25 percent) of the width
    090     * and height.
    091     */
    092    private double margin;
    093
    094    /** The angle increment for panning left and right (in radians). */
    095    private double panIncrement;
    096
    097    /** The angle increment for rotating up and down (in radians). */
    098    private double rotateIncrement;
    099
    100    /** The roll increment (in radians). */
    101    private double rollIncrement;
    102
    103    /**
    104     * The (screen) point of the last mouse click (will be {@code null}
    105     * initially).  Used to calculate the mouse drag distance and direction.
    106     */
    107    private Point lastClickPoint;
    108
    109    /**
    110     * The (screen) point of the last mouse move point that was handled.
    111     */
    112    private Point lastMovePoint;
    113
    114    /**
    115     * Temporary state to track the 2D offset during an ALT-mouse-drag
    116     * operation.
    117     */
    118    private Offset2D offsetAtMousePressed;
    119
    120    private RenderingInfo renderingInfo;
    121
    122    /**
    123     * Creates a new panel with the specified {@link Drawable3D} to
    124     * display.
    125     *
    126     * @param drawable  the content to display ({@code null} not
    127     *     permitted).
    128     */
    129    public Panel3D(Drawable3D drawable) {
    130        super(new BorderLayout());
    131        ArgChecks.nullNotPermitted(drawable, "drawable");
    132        this.drawable = drawable;
    133        this.margin = 0.25;
    134        this.minViewingDistance
    135                = drawable.getDimensions().getDiagonalLength();
    136        this.maxViewingDistanceMultiplier = 8.0;
    137        this.panIncrement = Math.PI / 60;
    138        this.rotateIncrement = Math.PI / 60;
    139        this.rollIncrement = Math.PI / 60;
    140        addMouseListener(this);
    141        addMouseMotionListener(this);
    142        addMouseWheelListener(this);
    143    }
    144
    145    /**
    146     * Returns the {@code Drawable3D} object that is displayed in this
    147     * panel.  This is specified via the panel constructor and there is no
    148     * setter method to change it.
    149     *
    150     * @return The {@code Drawable3D} object (never {@code null}).
    151     */
    152    public Drawable3D getDrawable() {
    153        return this.drawable;
    154    }
    155
    156    /**
    157     * Returns the margin, expressed as a percentage, that controls the amount
    158     * of space to leave around the edges of the 3D content when the
    159     * {@code zoomToFit()} method is called.  The default value is
    160     * {@code 0.25} (25 percent).
    161     *
    162     * @return The margin.
    163     */
    164    public double getMargin() {
    165        return this.margin;
    166    }
    167
    168    /**
    169     * Sets the margin that controls the amount of space to leave around the
    170     * edges of the 3D content when the {@code zoomToFit()} method is
    171     * called.
    172     *
    173     * @param margin  the margin (as a percentage, where 0.25 = 25 percent).
    174     */
    175    public void setMargin(double margin) {
    176        this.margin = margin;
    177    }
    178
    179    /**
    180     * Returns the minimum viewing distance.  Zooming by mouse wheel or other
    181     * means will not move the viewing point closer than this.  The value
    182     * is computed in the constructor from the dimensions of the drawable
    183     * object.
    184     *
    185     * @return The minimum viewing distance.
    186     */
    187    public double getMinViewingDistance() {
    188        return this.minViewingDistance;
    189    }
    190
    191    /**
    192     * Returns the multiplier for the maximum viewing distance (a multiple of
    193     * the minimum viewing distance).  The default value is {@code 8.0}.
    194     *
    195     * @return The multiplier.
    196     *
    197     * @since 1.3
    198     */
    199    public double getMaxViewingDistanceMultiplier() {
    200        return this.maxViewingDistanceMultiplier;
    201    }
    202
    203    /**
    204     * Sets the multiplier used to calculate the maximum viewing distance.
    205     *
    206     * @param multiplier  the new multiplier.
    207     *
    208     * @since 1.3
    209     */
    210    public void setMaxViewingDistanceMultiplier(double multiplier) {
    211        this.maxViewingDistanceMultiplier = multiplier;
    212    }
    213
    214    /**
    215     * Returns the angle delta for each pan left or right.  The default
    216     * value is {@code Math.PI / 60}.
    217     *
    218     * @return The angle delta (in radians).
    219     */
    220    public double getPanIncrement() {
    221        return panIncrement;
    222    }
    223
    224    /**
    225     * Sets the standard increment for panning left and right (a rotation
    226     * specified in radians).
    227     *
    228     * @param panIncrement  the increment (in radians).
    229     */
    230    public void setPanIncrement(double panIncrement) {
    231        this.panIncrement = panIncrement;
    232    }
    233
    234    /**
    235     * Returns the angle delta for each rotate up or down.  The default
    236     * value is {@code Math.PI / 60}.
    237     *
    238     * @return The angle delta (in radians).
    239     */
    240    public double getRotateIncrement() {
    241        return rotateIncrement;
    242    }
    243
    244    /**
    245     * Sets the vertical (up and down) rotation increment (in radians).
    246     *
    247     * @param rotateIncrement  the increment (in radians).
    248     */
    249    public void setRotateIncrement(double rotateIncrement) {
    250        this.rotateIncrement = rotateIncrement;
    251    }
    252
    253    /**
    254     * Returns the angle delta for each roll operation.  The default
    255     * value is {@code Math.PI / 60}.
    256     *
    257     * @return The angle delta (in radians).
    258     */
    259    public double getRollIncrement() {
    260        return rollIncrement;
    261    }
    262
    263    /**
    264     * Sets the roll increment in radians.
    265     *
    266     * @param rollIncrement  the increment (in radians).
    267     */
    268    public void setRollIncrement(double rollIncrement) {
    269        this.rollIncrement = rollIncrement;
    270    }
    271
    272    /**
    273     * Returns the view point that is maintained by the {@link Drawable3D}
    274     * instance on display.
    275     *
    276     * @return  The view point (never {@code null}).
    277     */
    278    public ViewPoint3D getViewPoint() {
    279        return this.drawable.getViewPoint();
    280    }
    281
    282    /**
    283     * Sets a new view point and repaints the panel.
    284     *
    285     * @param vp  the view point ({@code null} not permitted).
    286     */
    287    public void setViewPoint(ViewPoint3D vp) {
    288        ArgChecks.nullNotPermitted(vp, "vp");
    289        this.drawable.setViewPoint(vp);  //
    290        repaint();
    291    }
    292
    293    /**
    294     * Returns the last click point (possibly {@code null}).
    295     *
    296     * @return The last click point (possibly {@code null}).
    297     */
    298    protected Point getLastClickPoint() {
    299        return this.lastClickPoint;
    300    }
    301
    302    /**
    303     * Returns the rendering info from the previous call to
    304     * draw().
    305     *
    306     * @return The rendering info (possibly {@code null}).
    307     */
    308    protected RenderingInfo getRenderingInfo() {
    309        return this.renderingInfo;
    310    }
    311
    312    /**
    313     * Rotates the view point around from left to right by the specified
    314     * angle and repaints the 3D scene.  The direction relative to the
    315     * world coordinates depends on the orientation of the view point.
    316     *
    317     * @param angle  the angle of rotation (in radians).
    318     */
    319    public void panLeftRight(double angle) {
    320        this.drawable.getViewPoint().panLeftRight(angle);
    321        repaint();
    322    }
    323
    324    /**
    325     * Adjusts the viewing distance so that the chart fits the current panel
    326     * size.  A margin is left (see {@link #getMargin()} around the edges to
    327     * leave room for labels etc.
    328     */
    329    public void zoomToFit() {
    330        zoomToFit(getSize());
    331    }
    332
    333    /**
    334     * Adjusts the viewing distance so that the chart fits the specified
    335     * size.  A margin is left (see {@link #getMargin()} around the edges to
    336     * leave room for labels etc.
    337     *
    338     * @param size  the target size ({@code null} not permitted).
    339     */
    340    public void zoomToFit(Dimension2D size) {
    341        int w = (int) (size.getWidth() * (1.0 - this.margin));
    342        int h = (int) (size.getHeight() * (1.0 - this.margin));
    343        Dimension2D target = new Dimension(w, h);
    344        Dimension3D d3d = this.drawable.getDimensions();
    345        float distance = this.drawable.getViewPoint().optimalDistance(target,
    346                d3d, this.drawable.getProjDistance());
    347        this.drawable.getViewPoint().setRho(distance);
    348        repaint();
    349    }
    350
    351    /**
    352     * Paints the panel by asking the drawable to render a 2D projection of the
    353     * objects it is managing.
    354     *
    355     * @param g  the graphics target ({@code null} not permitted, assumed to be
    356     *     an instance of {@code Graphics2D}).
    357     */
    358    @Override
    359    public void paintComponent(Graphics g) {
    360        super.paintComponent(g);
    361        Graphics2D g2 = (Graphics2D) g;
    362        AffineTransform saved = g2.getTransform();
    363        Dimension size = getSize();
    364        Insets insets = getInsets();
    365        Rectangle drawArea = new Rectangle(insets.left, insets.top,
    366                size.width - insets.left - insets.right,
    367                size.height - insets.top - insets.bottom);
    368        this.renderingInfo = this.drawable.draw(g2, drawArea);
    369        g2.setTransform(saved);
    370    }
    371
    372    /**
    373     * Registers this component with the tool tip manager.
    374     *
    375     * @since 1.3
    376     */
    377    public void registerForTooltips() {
    378        ToolTipManager.sharedInstance().registerComponent(this);
    379    }
    380
    381    /**
    382     * Unregisters this component with the tool tip manager.
    383     *
    384     * @since 1.3
    385     */
    386    public void unregisterForTooltips() {
    387        ToolTipManager.sharedInstance().unregisterComponent(this);
    388    }
    389
    390    /* (non-Javadoc)
    391     * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
    392     */
    393    @Override
    394    public void mouseClicked(MouseEvent e) {
    395        // nothing to do
    396    }
    397
    398    /* (non-Javadoc)
    399     * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
    400     */
    401    @Override
    402    public void mouseEntered(MouseEvent e) {
    403        // nothing to do
    404    }
    405
    406    /* (non-Javadoc)
    407     * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
    408     */
    409    @Override
    410    public void mouseExited(MouseEvent e) {
    411        // nothing to do
    412    }
    413
    414    /* (non-Javadoc)
    415     * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
    416     */
    417    @Override
    418    public void mousePressed(MouseEvent e) {
    419        this.lastClickPoint = e.getPoint();
    420        this.lastMovePoint = this.lastClickPoint;
    421        this.offsetAtMousePressed = this.drawable.getTranslate2D();
    422    }
    423
    424    /* (non-Javadoc)
    425     * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
    426     */
    427    @Override
    428    public void mouseReleased(MouseEvent e) {
    429        // nothing to do
    430    }
    431
    432    /* (non-Javadoc)
    433     * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
    434     */
    435    @Override
    436    public void mouseDragged(MouseEvent e) {
    437        if (e.isAltDown()) {
    438            Point currPt = e.getPoint();
    439            Offset2D offset = this.offsetAtMousePressed;
    440            Point lastPt = getLastClickPoint();
    441            double dx = offset.getDX() + (currPt.x - lastPt.x);
    442            double dy = offset.getDY() + (currPt.y - lastPt.y);
    443            this.drawable.setTranslate2D(new Offset2D(dx, dy));
    444        } else {
    445            Point currPt = e.getPoint();
    446            int dx = currPt.x - this.lastMovePoint.x;
    447            int dy = currPt.y - this.lastMovePoint.y;
    448            this.lastMovePoint = currPt;
    449            this.drawable.getViewPoint().panLeftRight(-dx * Math.PI / 120);
    450            this.drawable.getViewPoint().moveUpDown(-dy * Math.PI / 120);
    451            repaint();
    452        }
    453    }
    454
    455    /* (non-Javadoc)
    456     * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
    457     */
    458    @Override
    459    public void mouseMoved(MouseEvent e) {
    460        // nothing to do
    461    }
    462
    463    /**
    464     * Receives notification of a mouse wheel movement and responds by moving
    465     * the viewpoint in or out (zooming).
    466     *
    467     * @param mwe  the mouse wheel event.
    468     */
    469    @Override
    470    public void mouseWheelMoved(MouseWheelEvent mwe) {
    471        float units = mwe.getUnitsToScroll();
    472        double maxViewingDistance = this.maxViewingDistanceMultiplier
    473                * this.minViewingDistance;
    474        double valRho = Math.max(this.minViewingDistance,
    475                Math.min(maxViewingDistance,
    476                this.drawable.getViewPoint().getRho() + units));
    477        this.drawable.getViewPoint().setRho(valRho);
    478        repaint();
    479    }
    480
    481    /**
    482     * Writes the current content to the specified file in PDF format.  This
    483     * will only work when the OrsonPDF library is found on the classpath.
    484     * Reflection is used to ensure there is no compile-time dependency on
    485     * OrsonPDF (which is non-free software).
    486     *
    487     * @param file  the output file ({@code null} not permitted).
    488     * @param w  the chart width.
    489     * @param h  the chart height.
    490     *
    491     * @deprecated Use ExportUtils.writeAsPDF() directly.
    492     */
    493    void writeAsPDF(File file, int w, int h) {
    494        ExportUtils.writeAsPDF(drawable, w, h, file);
    495    }
    496
    497    /**
    498     * Writes the current content to the specified file in SVG format.  This
    499     * will only work when the JFreeSVG library is found on the classpath.
    500     * Reflection is used to ensure there is no compile-time dependency on
    501     * JFreeSVG.
    502     *
    503     * @param file  the output file ({@code null} not permitted).
    504     * @param w  the chart width.
    505     * @param h  the chart height.
    506     *
    507     * @deprecated Use ExportUtils.writeAsPDF() directly.
    508     */
    509    void writeAsSVG(File file, int w, int h) {
    510        ExportUtils.writeAsSVG(this.drawable, w, h, file);
    511    }
    512
    513}