/*
* (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:jacob.dreyer@geosoft.no">Jacob Dreyer</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);
}
}