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.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 org.jfree.chart3d.internal.Args;
055import org.jfree.chart3d.export.ExportUtils;
056import org.jfree.chart3d.graphics3d.Dimension3D;
057import org.jfree.chart3d.graphics3d.Drawable3D;
058import org.jfree.chart3d.graphics3d.Offset2D;
059import org.jfree.chart3d.graphics3d.RenderingInfo;
060import org.jfree.chart3d.graphics3d.ViewPoint3D;
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 final Drawable3D drawable;
079    
080    /** 
081     * The minimum viewing distance (zooming in will not go closer than this).
082     */
083    private final 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        Args.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        Args.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}