/* * (C) 2004 - Geotechnical Software Services * * This code is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program; if not, write to the Free * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, * MA 02111-1307, USA. */ package no.geosoft.cc.graphics; import java.util.Collection; import java.awt.Adjustable; import no.geosoft.cc.geometry.Rect; /** * The GScene is the link between a GWindow and the graphics objects. * <p> * The GScene defines the viewport and the world extent and holds * device-to-world transformation objects. The scene is itself a * graphics object (GObject) and as such it may contain geometry. * <p> * Typical usage: * * <pre> * // Creating a window * GWindow window = new GWindow (Color.WHITE); * * // Creating a scene within the window * GScene scene = new GScene (window); * scene.setWorldExtent (0.0, 0.0, 1000.0, 1000.0); * </pre> * * Setting world extent is optional. If unset it will have the same * extent (in floating point coordinates) as the device. * <p> * When geometry is specified (in GSegments), coordinates are specified * in either device coordinates or in world coordinates. Integer coordinates * are assumed to be device relative, while floating point coordinates are * taken to be world extent relative. * * @author <a href="mailto:info@geosoft.no">GeoSoft</a> */ public class GScene extends GObject { private final GWindow window_; private final GTransformer transformer_; private final GAnnotator annotator_; private GViewport viewport_; private GWorldExtent initialWorldExtent_; private GWorldExtent currentWorldExtent_; private boolean shouldZoomOnResize_; // "see more" or "see bigger" private boolean shouldWorldExtentFitViewport_; // ie. squeeze world private boolean isAnnotationValid_; private GScrollHandler scrollHandler_; private boolean isViewportFixed_; /** * Create a scene within the specified window. For most practical * purposes a window will contain one scene only. * <p> * A default viewport will be established covering the entire window. * This is appropriate in most cases. * <p> * A default world-extent will be established with coordinates equal * to the window device extent. This may be sufficient in many cases * if object geometry is specified in device coordinates. * * @param window Window to attach this scene to. * @param name Name of this scene. */ public GScene (GWindow window, String name) { super (name); shouldWorldExtentFitViewport_ = true; shouldZoomOnResize_ = true; // "see bigger" // Attach to window window_ = window; window_.addScene (this); // Default viewport equals window int viewportWidth = window_.getWidth(); int viewportHeight = window_.getHeight(); viewport_ = new GViewport (0, 0, viewportWidth, viewportHeight); isViewportFixed_ = false; // Default world extent equals window double w0[] = {0.0, (double) viewportHeight, 0.0}; double w1[] = {(double) viewportWidth, (double) viewportHeight, 0.0}; double w2[] = {0.0, 0.0, 0.0}; initialWorldExtent_ = new GWorldExtent (w0, w1, w2); currentWorldExtent_ = new GWorldExtent (w0, w1, w2); // Create transformer instance transformer_ = new GTransformer (viewport_, currentWorldExtent_); // Instantiate the annotator object annotator_ = new GAnnotator (this); scrollHandler_ = null; // Initiate region updateRegion(); } /** * Create a nameless scene within the specified window. * * @param window Window to acttach this scene to. */ public GScene (GWindow window) { this (window, null); } /** * Return the scene of this GObject. At this level the scene is * this object. * * @return The scene of this GObject. */ public GScene getScene() { return this; } /** * Return the window of this scene. A scene is always attached to * a window, a GObject is not necesserily. * * @return Window of this scene. */ public GWindow getWindow() { return window_; } /** * Return the transformation object of this scene. The transformer * object can be used for client-side world-to-device and device-to-world * coordinate transformations. * * @return Current transformation object of this scene. */ public GTransformer getTransformer() { return transformer_; } /** * Specified if one should zoom on resize. Set to true, the current * image will change size (i.e. zoomed). Set to false the current image * will be the same size and one will possibley see more or less * instead. Default is true. * * @param shouldZoomOnResize True if world extent should be unchanged * on resize, false otherwise. */ public void shouldZoomOnResize (boolean shouldZoomOnResize) { shouldZoomOnResize_ = shouldZoomOnResize; } /** * Specify wether world extent should always fit the viewport (i.e. have * same aspect ratio) during resize. Default is true. * * @param shouldWorldExtentFitViewport True if the world extent should * fit the viewport, false otherwise. */ public void shouldWorldExtentFitViewport (boolean shouldWorldExtentFitViewport) { shouldWorldExtentFitViewport_ = shouldWorldExtentFitViewport; } /** * Set viewport for this scene. The viewport is specified in device * coordinates. The layout is as follows: * * <pre> * * x0,y0 o-------o x1,y1 * | * | * | * x2,y2 o * * </pre> * * It is thus possible to create a skewed viewport, which may be handy * in some situations. * <p> * If the viewport is not set by a client, it will fit the canvas and * adjust to it during resize. If it is set by client, it will stay * fixed and not adjusted on resize. */ public void setViewport (int x0, int y0, int x1, int y1, int x2, int y2) { isViewportFixed_ = true; // Flag old viewport as damaged // TODO: If the viewport is moved around in the canvas during // execution (rare), old content outside the new viewport will // remain as redraw is clipped against the new viewport. window_.updateDamageArea (getRegion()); // Update viewport viewport_.set (x0, y0, x1, y1, x2, y2); // Set the new region for this scene updateRegion(); // Flag new viewport as damaged window_.updateDamageArea (getRegion()); adjustCurrentWorldExtent(); transformer_.update (viewport_, currentWorldExtent_); // Redraw annotator_.reset(); redraw (getVisibility()); // Update scrollbars if (scrollHandler_ != null) scrollHandler_.updateScrollBars(); } /** * Set viewport to a rectangular area of the screen. The viewport * layout is as follows: * * <pre> * * width * x0,y0 o-------o * | * height | * | * o * * </pre> * * @param x0 X coordinate of upper left corner of viewport. * @param y0 Y coordinate of upper left corner of viewport. * @param width Width of viewport. * @param height Height of viewport. */ public void setViewport (int x0, int y0, int width, int height) { setViewport (x0, y0, x0+width-1, y0, x0, y0+height-1); } /** * Return current viewport. * * @return Current viewport of this scene. */ public GViewport getViewport() { return viewport_; } /** * Set world extent of this scene. The layout is as follows: * * <pre> * w2 o * | * | * | * w0 o-------o w1 * </pre> * * Thus w2 is mapped to viewport (x0,y0), w0 is mapped to (x2,y2) and * w1 is mapped to lower right corner of viewport. * <p> * w0,w1 and w2 are three dimensions, and the world extent can thus be * any plane in a 3D space, and the plane may be skewed. * * @param w0 Point 0 of the new world extent [x,y,z]. * @param w1 Point 1 of the new world extent [x,y,z]. * @param w2 Point 2 of the new world extent [x,y,z]. */ public void setWorldExtent (double w0[], double w1[], double w2[]) { initialWorldExtent_.set (w0, w1, w2); currentWorldExtent_.set (w0, w1, w2); adjustCurrentWorldExtent(); transformer_.update (viewport_, currentWorldExtent_); // Flag new viewport as damaged window_.updateDamageArea (getRegion()); redraw (getVisibility()); // Update scrollbars if (scrollHandler_ != null) scrollHandler_.updateScrollBars(); } /** * A convenience method for specifying a orthogonal world extent in * the Z=0 plane. The layout is as follows: * * <pre> * o * | * height | * | * x0,y0 o-------o * width * * </pre> * * @param x0 X coordinate of world extent origin. * @param y0 Y coordinate of world extent origin. * @param width Width of world extent. * @param height Height of world extent. */ public void setWorldExtent (double x0, double y0, double width, double height) { double w0[] = {x0, y0, 0.0}; double w1[] = {x0 + width, y0, 0.0}; double w2[] = {x0, y0 + height, 0.0}; setWorldExtent (w0, w1, w2); } /** * Return the world extent as specified by the application. * * @return The world extent as it was specified through setWorldExtent(). */ GWorldExtent getInitialWorldExtent() { return initialWorldExtent_; } /** * Return current world extent. The current world extent may differ the * initial world extent due to zooming and window resizing. * * @return Current world extent. */ public GWorldExtent getWorldExtent() { return currentWorldExtent_; } /** * Adjust the current world extent according to current viewport. * This method is called whenever the viewport has changed. */ private void adjustCurrentWorldExtent() { // Do nothing if the world extent is supposed to fit viewport if (shouldWorldExtentFitViewport_) return; // Viewport dimensions double viewportWidth = (double) viewport_.getWidth(); double viewportHeight = (double) viewport_.getHeight(); // World dimensions double worldWidth = currentWorldExtent_.getWidth(); double worldHeight = currentWorldExtent_.getHeight(); // Compute adjusted width or height double newWorldWidth; double newWorldHeight; if (worldWidth / worldHeight > viewportWidth / viewportHeight) { newWorldWidth = worldWidth; newWorldHeight = viewportHeight / viewportWidth * worldWidth; } else { newWorldWidth = viewportWidth / viewportHeight * worldHeight; newWorldHeight = worldHeight; } currentWorldExtent_.extendWidth (newWorldWidth); currentWorldExtent_.extendHeight (newWorldHeight); } /** * Update region for this GObject. The region of a GScene is always the * viewport extent. */ private void updateRegion() { if (viewport_.isSkewed()) { // TODO. Missing the create Region of a general polygon, // this case is special though and can be hacked by adding // a rectangle for each scan line in the viewport. } else { Rect rectangle = new Rect (viewport_.getX0(), viewport_.getY0(), (int) viewport_.getWidth(), (int) viewport_.getHeight()); getRegion().set (rectangle); } flagRegionValid (true); } /** * Resize this scene the specified fraction in x and y direction. * <p> * If a client uses scenes wich covers a specific part of a window, * it may want to extend GScene and override this method in order to * adjust the viewport according to the new window size. This can * be done as follows: * * <pre> * protected void resize (double dx, double dy) * { * super (dx, dy); * setViewport (...); * } * </pre> * * @param dx Resize fraction in x direction. * @param dy Resize fraction in y direction. */ protected void resize (double dx, double dy) { if (isViewportFixed_) return; // Resize viewport viewport_.resize (dx, dy); // Resize world extent if (!shouldWorldExtentFitViewport_) { // If we resize the world extents accordingly we will see more // than before, in same scale, thus "see more". // If we keep the world extent unchanged we will see the same // extent as before but rescaled, thus "see bigger" if (!shouldZoomOnResize_) { initialWorldExtent_.resize (dx, dy); currentWorldExtent_.resize (dx, dy); } adjustCurrentWorldExtent(); } transformer_.update (viewport_, currentWorldExtent_); // Compute new region updateRegion(); if (scrollHandler_ != null) scrollHandler_.updateScrollBars(); } /** * Zoom a specified amount around center of viewport. * * @param zoomFactor Zoom factor. Zoom in with factor < 1.0 and * out with factor > 1.0. */ public void zoom (double zoomFactor) { double x = viewport_.getCenterX(); double y = viewport_.getCenterY(); zoom ((int) Math.round (x), (int) Math.round (y), zoomFactor); } /** * Zoom a specific amount using specified point as fixed. * * <ul> * <li> Zoom in: zoom (x, y, 0.9); * <li> Zoom out: zoom (x, y, 1.1); * <li> etc. * </ul> * * @param x X coordinate of fixed point during zoom. * @param y Y coordinate of fixed point during zoom. * @param zoomFactor Zoom factor. */ public void zoom (int x, int y, double zoomFactor) { int x0 = viewport_.getX0(); int y0 = viewport_.getY0(); int x1 = viewport_.getX3(); int y1 = viewport_.getY3(); double width = viewport_.getWidth(); double height = viewport_.getHeight(); x0 += (1.0 - zoomFactor) * (x - x0); x1 -= (1.0 - zoomFactor) * (x1 - x); y0 += (1.0 - zoomFactor) * (y - y0); y1 -= (1.0 - zoomFactor) * (y1 - y); zoom (x0, y0, x1, y1); } /** * Zoom into a specific device area. * * @param x0 X value of first corner of zoom rectangle. * @param y0 Y value of first corner of zoom rectangle. * @param x1 X value of second corner of zoom rectangle. * @param y1 Y value of second corner of zoom rectangle. */ public void zoom (int x0, int y0, int x1, int y1) { // Make sure x0,y0 is upper left and x1,y1 is lower right if (x1 < x0) { int temp = x1; x1 = x0; x0 = temp; } if (y1 < y0) { int temp = y1; y1 = y0; y0 = temp; } // Tranform to world double w0[] = transformer_.deviceToWorld (x0, y1); double w1[] = transformer_.deviceToWorld (x1, y1); double w2[] = transformer_.deviceToWorld (x0, y0); zoom (w0, w1, w2); } /** * Zoom into a specified world area. * * @param w0 First world coordinate of zoom area [x,y,z]. * @param w1 Second world coordinate of zoom area [x,y,z]. * @param w2 Third world coordinate of zoom area [x,y,z]. */ public void zoom (double w0[], double w1[], double w2[]) { // Set new world extent currentWorldExtent_.set (w0, w1, w2); // Flag entire scene as damaged window_.updateDamageArea (getRegion()); // Make sure we keep aspect ratio (if required) adjustCurrentWorldExtent(); // Update the transformer transformer_.update (viewport_, currentWorldExtent_); // Redraw all affected elements window_.redraw(); // Rerender the graphics window_.refresh(); // Update scrollbars if present if (scrollHandler_ != null) scrollHandler_.updateScrollBars(); } /** * Unzoom. Unzooming sets the current world extent back to the initial * world extent as specified by the client application by setWorldExtent(). */ public void unzoom() { zoom (initialWorldExtent_.get(0), initialWorldExtent_.get(1), initialWorldExtent_.get(2)); } /** * Pan a specific device distance. * * @param dx Distance to pan in x direction. * @param dy Distance to pan in y direction. */ public void pan (int dx, int dy) { int x0 = viewport_.getX0() - dx; int y0 = viewport_.getY0() - dy; int x1 = viewport_.getX3() - dx; int y1 = viewport_.getY3() - dy; zoom (x0, y0, x1, y1); } /** * Flag the annotation of this scene as valid or invalid. Annotation * is set to invalid if annotation is changed somewhere down the tree. * This is an instruction to the GWindow to redo the annotation on this * scene. When the annotation is redone, this flag is set to valid. * * @param isAnnotationValid True if the annotation of this scene is valid * false otherwise. */ void setAnnotationValid (boolean isAnnotationValid) { isAnnotationValid_ = isAnnotationValid; } /** * Check if annotation in this scene is valid. * * @return True if the annotation is valid, false otherwise. */ boolean isAnnotationValid() { return isAnnotationValid_; } /** * Compute positions of all text (GText) elements in this scene. */ void computeTextPositions() { annotator_.reset(); super.computeTextPositions(); isAnnotationValid_ = true; } /** * Compute positions of all AWT components (GComponent) elements * in this scene. */ void computeComponentPositions() { super.computeComponentPositions(); } /** * Compute positions of the specified positionals. * * @param positionals Positionals to compute positions of. */ void computePositions (Collection positionals) { annotator_.computePositions (positionals); } /** * Compute positions for positional object that are attached * to every vertex of its owner. * * @param positional Positional to compute position for. */ void computeVertexPositions (GPositional positional) { annotator_.computeVertexPositions (positional); } /** * Instruct this scene to update and respond to the specified * scrollbars during zoom. * <p> * <b>NOTE I:</b> The client application is responsible for laying out * the scrollbars in the AWT/Swing GUI. The scrollbars should have no * access listeneres nor logic added, as this is controlled by the * GScene through the internal GScrollHandler object. * <p> * <b>NOTE II:</b> Do not put the graphics panel in a JScrollPane and use * the JScrollPane scrollbars as input to this method, as a JScrollPane * contains scroll logic that interfer with the internal GScene logic. * The correct approach is to create horizontal and vertical JScrollBar * explicitly. * <p> * Specifying both horizontal and vertical scrollbar as <em>null</em> * turns off scroll handling in this scene. * * @param hScrollBar Horizontal scrollbar (or null if a horizontal * scrollbar is not used). * @param hScrollBar Vertical scrollbar (or null if a vertical scrollbar * is not used). */ public void installScrollHandler (Adjustable hScrollBar, Adjustable vScrollBar) { if (hScrollBar == null && vScrollBar == null) scrollHandler_ = null; else scrollHandler_ = new GScrollHandler (this, hScrollBar, vScrollBar); } }