All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.javagl.viewer.Viewer Maven / Gradle / Ivy

The newest version!
/*
 * www.javagl.de - Viewer
 *
 * Copyright (c) 2013-2015 Marco Hutter - http://www.javagl.de
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package de.javagl.viewer;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import javax.swing.JPanel;

import de.javagl.geom.AffineTransforms;
import de.javagl.geom.Points;
import de.javagl.geom.Rectangles;

/**
 * A panel that allows translating, rotating and zooming. 
*
* Instances of classes implementing the {@link Painter} interface may be * added to this viewer and perform painting operations. Their * {@link Painter#paint(Graphics2D, AffineTransform, double, double) paint} * method will receive the world-to-screen transform, as determined by the * current translation, rotation and zoom of the viewer.
*
* The viewer supports multiple layers. The layer may be specified when a * {@link Painter} is added using the {@link #addPainter(Painter, int)} * method. The {@link Painter} instances will be called starting at the * lowest layer. Inside one layer, the {@link Painter}s will be called in * the order in which they have been added.
*
* A viewer has several methods that affect the translation, rotation and * zoom factor:
*
*
    *
  • {@link #translate(double, double)}
  • *
  • {@link #rotate(double, double, double)}
  • *
  • {@link #zoom(double, double, double, double)}
  • *
* These methods will usually not be called by clients or users of the * viewer. Instead, they are called by a class that implements the * {@link MouseControl} interface, and has been set using * {@link #setMouseControl(MouseControl)}. These classes are responsible for * making sure that these methods are called with valid parameters. * Particularly, they should not be called with NaN or * infinite values, and the zooming factors should not be 0.0. The viewer * class itself does not perform any sanity checks on these arguments. */ public class Viewer extends JPanel { /** * Serial UID */ private static final long serialVersionUID = -3252732941609348700L; /** * The map from each layer to the set of {@link Painter}s that will * perform painting operations in the {@link #paintComponent(Graphics)} * method. */ private final Map> painters; /** * The transformation of this viewer */ private final AffineTransform transform; /** * The inverse transform, computed on demand, and set to null * when it is invalidated. */ private AffineTransform inverseTransform = null; /** * The area in world coordinates covered by this panel */ private final Rectangle2D worldArea; /** * A pending size for the world area. When * {@link #setDisplayedWorldArea(Rectangle2D)} is called before * this component is visible, the intended area has to be stored * until the component becomes visible and the area update can * actually be applied. */ private Rectangle2D pendingWorldArea = null; /** * The state of the {@link #maintainAspectRatio} flag when the * {@link #pendingWorldArea} was set */ private boolean pendingWorldAreaMaintainAspectRatioState = false; /** * The last stored screen size */ private Dimension previousSize = null; /** * Whether the y-axis should be flipped to point from the * bottom of the screen to the top */ private boolean flippedVertically = false; /** * A transform that will be concatenated with the world-to-screen * transform, for example, to flip the y-axis vertically */ private AffineTransform basicWorldToScreenTransform = null; /** * Whether the contents should be resized when the screen * is resized */ private boolean resizingContents = false; /** * This flag determines whether the aspect ratio should be maintained * during resize operations (when resizingContents is true) * or when {@link #setDisplayedWorldArea(Rectangle2D)} is called. */ private boolean maintainAspectRatio = true; /** * Whether the antialiasing rendering hint should be enabled * by default */ private boolean antialiasing = true; /** * The {@link MouseControl} that is attached to this viewer */ private MouseControl mouseControl; /** * Creates a new Viewer.
*
* The default settings are as follows: *
    *
  • {@link #isResizingContents()} is false
  • *
  • {@link #isMaintainAspectRatio()} is true
  • *
  • {@link #isAntialiasing()} is true
  • *
* A default {@link MouseControls#createDefault(Viewer, boolean, boolean) * MouseControl}, where rotation and non-uniform scaling are allowed, * will be installed by calling {@link #setMouseControl(MouseControl)} */ public Viewer() { this.painters = new TreeMap>(); this.transform = new AffineTransform(); this.inverseTransform = new AffineTransform(); this.worldArea = new Rectangle2D.Double(0,0,1,1); setMouseControl( MouseControls.createDefault(this, true, true)); setBackground(Color.WHITE); addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { handleResize(); repaint(); } }); } /** * Set the given {@link MouseControl} as a MouseListener, * MouseMotionListener and MouseWheelListener * for this viewer. Any previously registered {@link MouseControl} will * be removed. * * @param newMouseControl The {@link MouseControl} to set. This may be * null to remove any {@link MouseControl} */ public final void setMouseControl(MouseControl newMouseControl) { if (mouseControl != null) { removeMouseListener(mouseControl); removeMouseMotionListener(mouseControl); removeMouseWheelListener(mouseControl); } this.mouseControl = newMouseControl; if (mouseControl != null) { addMouseListener(mouseControl); addMouseMotionListener(mouseControl); addMouseWheelListener(mouseControl); } } /** * Set whether the y-axis should be flipped to point from the * bottom of the screen to the top * * @param flippedVertically Whether the y-axis should be flipped */ public final void setFlippedVertically(boolean flippedVertically) { if (flippedVertically) { basicWorldToScreenTransform = getFlipVerticallyTransform(); } else { basicWorldToScreenTransform = null; } this.flippedVertically = flippedVertically; repaint(); } /** * Set whether the y-axis is flipped to point from the bottom of the * screen to the top * * @return Whether the y-axis is be flipped */ public final boolean isFlippedVertically() { return flippedVertically; } /** * Set whether the contents should be resized when the screen * is resized. When this is set to true, then resizing * this panel will adjust the scale factors accordingly, so that * the world area that is displayed remains the same while resizing.
*
* Note that this usually only makes sense when the aspect ratio is not * maintained - that is, when {@link #setMaintainAspectRatio(boolean)} * was set to false. * * @param resizingContents The resizing behavior */ public final void setResizingContents(boolean resizingContents) { this.resizingContents = resizingContents; } /** * Returns whether this viewer is configured so that the contents * should be resized when the screen is resized. See * {@link #setResizingContents(boolean)}. * * @return The resizing behavior */ public final boolean isResizingContents() { return resizingContents; } /** * Set whether the aspect ratio of the displayed world area is * maintained during resize operations of this panel, or when * {@link #setDisplayedWorldArea(Rectangle2D)} is called.
*
* Note that this should usually be set to false when * {@link #setResizingContents(boolean)} is set to true. * * @param maintainAspectRatio Whether the aspect ratio is maintained */ public final void setMaintainAspectRatio(boolean maintainAspectRatio) { this.maintainAspectRatio = maintainAspectRatio; } /** * Returns whether the aspect ratio is maintained during resize operations * or when {@link #setDisplayedWorldArea(Rectangle2D)} is called * * @return Whether the aspect ratio is maintained during resize operations */ public final boolean isMaintainAspectRatio() { return maintainAspectRatio; } /** * Set whether the antialiasing rendering hint should be enabled * by default * * @param antialiasing Whether antialiasing should be enabled */ public final void setAntialiasing(boolean antialiasing) { this.antialiasing = antialiasing; } /** * Return whether the antialiasing rendering hint is enabled * by default * * @return Whether antialiasing is enabled */ public final boolean isAntialiasing() { return antialiasing; } /** * Add the given {@link Painter}, which will perform painting * operations in the {@link #paintComponent(Graphics)} method. * The painter will be added at the default layer (0). If the * given painter is null, then this call will have * no effect and false will be returned. * * @param painter The {@link Painter} to add * @return Whether the painter was not yet contained in this viewer */ public final boolean addPainter(Painter painter) { return addPainter(painter, 0); } /** * Add the given {@link Painter}, which will perform painting * operations in the {@link #paintComponent(Graphics)} method, * on the specified layer. If the given painter is null, * then this call will have no effect and false will be * returned. * * @param painter The {@link Painter} to add * @param layer The layer for the {@link Painter} * @return Whether the painter was not yet contained in this viewer */ public final boolean addPainter(Painter painter, int layer) { if (painter == null) { return false; } Set set = painters.get(layer); if (set == null) { set = new LinkedHashSet(); painters.put(layer, set); } boolean changed = set.add(painter); if (changed) { repaint(); } return changed; } /** * Remove the given {@link Painter} from all layers that it is contained * in * * @param painter The {@link Painter} to remove * @return Whether the painter was contained in this viewer */ public final boolean removePainter(Painter painter) { boolean changed = false; Set toRemove = new LinkedHashSet(); for (Entry> entry : painters.entrySet()) { Set set = entry.getValue(); boolean wasContained = set.remove(painter); if (wasContained) { changed = true; if (set.isEmpty()) { toRemove.add(entry.getKey()); } } } if (!toRemove.isEmpty()) { painters.keySet().removeAll(toRemove); } if (changed) { repaint(); } return changed; } /** * Remove the given {@link Painter} from the specified layer * * @param painter The {@link Painter} to remove * @param layer The layer from which the painter should be removed * @return Whether the painter was contained in this layer */ public final boolean removePainter(Painter painter, int layer) { Set set = painters.get(layer); boolean wasContained = set.remove(painter); if (wasContained) { if (set.isEmpty()) { painters.remove(layer); } repaint(); return true; } return false; } @Override protected void paintComponent(Graphics gr) { super.paintComponent(gr); Graphics2D g = (Graphics2D)gr; if (pendingWorldArea != null) { boolean b = isMaintainAspectRatio(); setMaintainAspectRatio( pendingWorldAreaMaintainAspectRatioState); setDisplayedWorldArea(pendingWorldArea); pendingWorldArea = null; setMaintainAspectRatio(b); } if (antialiasing) { g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } else { g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); } for (Entry> entry : painters.entrySet()) { Set set = entry.getValue(); for (Painter painter : set) { AffineTransform w = getWorldToScreen(); painter.paint(g, w, getWidth(), getHeight()); } } } /** * Returns a copy of the current world-to-screen transform * * @return The world-to-screen transform */ public final AffineTransform getWorldToScreen() { AffineTransform at = new AffineTransform(transform); if (basicWorldToScreenTransform != null) { at.concatenate(basicWorldToScreenTransform); } return at; } /** * Returns an affine transform that flips the contents vertically * * @return The affine transform */ private static AffineTransform getFlipVerticallyTransform() { AffineTransform at = new AffineTransform(); at.concatenate(AffineTransform.getTranslateInstance(0, 1)); at.concatenate(AffineTransform.getScaleInstance(1, -1)); return at; } /** * Returns a copy of the screen-to-world transform * * @return The screen-to-world transform */ public final AffineTransform getScreenToWorld() { return new AffineTransform(getInverseTransform()); } /** * Returns the inverse transform, creating it if necessary * * @return The inverse transform */ private final AffineTransform getInverseTransform() { if (inverseTransform == null) { try { inverseTransform = getWorldToScreen().createInverse(); } catch (NoninvertibleTransformException e) { throw new IllegalArgumentException( "Non-invertible transform", e); } } return inverseTransform; } /** * Transform the contents of this viewer with the given transform * * @param t The transform * @throws NullPointerException if the given transform is null * @throws IllegalArgumentException if the determinant of the given * transform is 0.0, or NaN, or infinite */ public final void transform(AffineTransform t) { validate(t); transform.concatenate(t); inverseTransform = null; repaint(); } /** * Reset this viewer to the identity transform */ public final void resetTransform() { transform.setToIdentity(); inverseTransform = null; repaint(); } /** * Set the world-to-screen transform of this viewer to the given transform. * * @param t The transform to set * @throws NullPointerException if the given transform is null * @throws IllegalArgumentException if the determinant of the given * transform is 0.0, or NaN, or infinite */ public final void setTransform(AffineTransform t) { validate(t); transform.setTransform(t); inverseTransform = null; repaint(); } /** * Zoom about the specified point (in screen coordinates) by the given * factor.
*
* This method will try to limit the zooming factor of this viewer in * order to prevent rendering errors: When the zooming factor already * is very large, then this method may not allow zooming in any further * (and similarly, for zooming out when the zooming factor already * is very small). But due to the limited precision of double * computations, this limitation of the zoom may not always be * effective - for example, when the translation is very large. * * @param screenCenterX The x-coordinate of the zooming center, * in screen coordinates * @param screenCenterY The y-coordinate of the zooming center, * in screen coordinates * @param factorX The zooming factor for the x-axis * @param factorY The zooming factor for the y-axis */ public final void zoom(double screenCenterX, double screenCenterY, double factorX, double factorY) { if (zoomExceedsLimits(factorX, factorX)) { return; } Point2D worldCenter = Points.inverseTransform( transform, new Point2D.Double(screenCenterX, screenCenterY), null); AffineTransform t = new AffineTransform(); t.translate(worldCenter.getX(), worldCenter.getY()); t.scale(factorX, factorY); t.translate(-worldCenter.getX(), -worldCenter.getY()); transform(t); repaint(); } /** * Returns whether a {@link #zoom} with the given factors will exceed * the limits. This is the case when the zoom is already very large * or very small, and the additional zoom factors will likely introduce * rendering errors. * * @param factorX The zooming factor for the x-axis * @param factorY The zooming factor for the y-axis * @return Whether the zoom exceeds the limits */ private boolean zoomExceedsLimits(double factorX, double factorY) { final double maxScale = 1e8; final double minScale = 1e-8; double dx = AffineTransforms.computeDistanceX(transform, 1.0); double dy = AffineTransforms.computeDistanceY(transform, 1.0); if (dx > maxScale && factorX > 1.0) { return true; } if (dy > maxScale && factorY > 1.0) { return true; } if (dx < minScale && factorX < 1.0) { return true; } if (dy < minScale && factorY < 1.0) { return true; } return false; } /** * Translate this viewer by the given delta, in screen * coordinates * * @param screenDx The movement delta in x-direction, in screen coordinates * @param screenDy The movement delta in y-direction, in screen coordinates */ public final void translate(double screenDx, double screenDy) { Point2D worldOld = Points.inverseTransform( transform, new Point2D.Double(0, 0), null); Point2D worldNew = Points.inverseTransform( transform, new Point2D.Double(screenDx, screenDy), null); double tdx = worldNew.getX() - worldOld.getX(); double tdy = worldNew.getY() - worldOld.getY(); AffineTransform t = new AffineTransform(); t.translate(tdx, tdy); transform(t); repaint(); } /** * Rotate about the specified point (in screen coordinates) * by the given angle (in radians) * * @param screenCenterX The x-coordinate of the zooming center, * in screen coordinates * @param screenCenterY The y-coordinate of the zooming center, * in screen coordinates * @param angleRad The angle, in radians */ public final void rotate( double screenCenterX, double screenCenterY, double angleRad) { transform.preConcatenate( AffineTransform.getRotateInstance( angleRad, screenCenterX, screenCenterY)); inverseTransform = null; repaint(); } /** * Set the area (in world coordinates) that should be shown.
*
* This will adjust the scaling and translation so that at least the * specified rectangle is visible (even when the view is rotated).
*
* If this viewer is {@link #isMaintainAspectRatio() maintaining * the aspect ratio}, then the smallest area with the current * aspect ratio will be visible that entirely contains the specified * rectangle. * * @param x The x-coordinate * @param y The y-coordinate * @param w The width * @param h The height */ public final void setDisplayedWorldArea( double x, double y, double w, double h) { setDisplayedWorldArea(new Rectangle2D.Double(x, y, w, h)); } /** * Set the area (in world coordinates) that should be shown.
*
* This will adjust the scaling and translation so that at least the * given rectangle is visible (even when the view is rotated).
*
* If this viewer is {@link #isMaintainAspectRatio() maintaining * the aspect ratio}, then the smallest area with the current * aspect ratio will be visible that entirely contains the given * rectangle. * * @param newWorldArea The world area */ public final void setDisplayedWorldArea(Rectangle2D newWorldArea) { if (getWidth() <= 0 || getHeight() <= 0) { pendingWorldArea = new Rectangle2D.Double(); pendingWorldArea.setRect(newWorldArea); pendingWorldAreaMaintainAspectRatioState = maintainAspectRatio; return; } pendingWorldArea = null; worldArea.setRect(newWorldArea); Rectangle2D worldAreaInScreen = Rectangles.computeBounds( getWorldToScreen(), worldArea, null); double scaleX = getWidth() / worldAreaInScreen.getWidth(); double scaleY = getHeight() / worldAreaInScreen.getHeight(); double dx = -worldAreaInScreen.getX(); double dy = -worldAreaInScreen.getY(); if (maintainAspectRatio) { scaleX = Math.min(scaleX, scaleY); scaleY = scaleX; } transform.preConcatenate( AffineTransform.getTranslateInstance(dx, dy)); transform.preConcatenate( AffineTransform.getScaleInstance(scaleX, scaleY)); transform.preConcatenate( AffineTransform.getTranslateInstance(-dx * scaleX, -dy * scaleY)); inverseTransform = null; Rectangle2D newWorldAreaInScreen = Rectangles.computeBounds( getWorldToScreen(), worldArea, null); double newDx = -newWorldAreaInScreen.getX(); double newDy = -newWorldAreaInScreen.getY(); translate(newDx, newDy); repaint(); } /** * Update the scaling factor when this component was resized */ private void handleResize() { if (getWidth() <= 0 || getHeight() <= 0) { return; } if (pendingWorldArea != null) { boolean b = isMaintainAspectRatio(); setMaintainAspectRatio( pendingWorldAreaMaintainAspectRatioState); setDisplayedWorldArea(pendingWorldArea); pendingWorldArea = null; setMaintainAspectRatio(b); pendingWorldArea = null; } if (resizingContents) { if (previousSize == null) { previousSize = getSize(); } double scaleX = (double)getWidth() / previousSize.width; double scaleY = (double)getHeight() / previousSize.height; if (maintainAspectRatio) { scaleX = Math.min(scaleX, scaleY); scaleY = scaleX; } transform.preConcatenate( AffineTransform.getScaleInstance(scaleX, scaleY)); inverseTransform = null; } previousSize = getSize(); } /** * Make sure that the given determinant is non-null and has a * valid (and non-zero) determinant * * @param at The affine transform * @throws NullPointerException If the given transform is null * @throws IllegalArgumentException If the determinant of the given * transform is 0.0, or NaN, or infinite */ private static void validate(AffineTransform at) { double determinant = at.getDeterminant(); if (determinant == 0.0 || Double.isNaN(determinant) || Double.isInfinite(determinant)) { throw new IllegalArgumentException("Determinant is "+determinant); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy