/*
* (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.awt.Color;
import java.awt.Component;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import no.geosoft.cc.geometry.Region;
import no.geosoft.cc.geometry.Rect;
import no.geosoft.cc.geometry.Geometry;
/**
* GWindow is the top level graphics node and holder of GScene nodes
* (node containing world-to-device transformation). The GWindow is
* linked to the GUI through its canvas object.
* <p>
* Typical usage:
*
* <pre>
* // Some Swing component to hold the graphics
* JPanel panel = new JPanel();
* panel.setLayout (new BorderLayout());
*
* // Create the window and attach to GUI
* GWindow window = new GWindow (Color.WHITE);
* panel.add (window.getCanvas(), BorderLayout.CENTER);
* </pre>
*
* GWindow is also the holder of the current "interaction" object
* communicating mouse events between the back-end AWT component and
* the client application.
*
* @author <a href="mailto:info@geosoft.no">GeoSoft</a>
*/
public class GWindow
{
public static final int ABORT = 1;
public static final int MOTION = 2;
public static final int BUTTON1_DOWN = 3;
public static final int BUTTON1_DRAG = 4;
public static final int BUTTON1_UP = 5;
public static final int BUTTON1_DOUBLE_CLICK = 6; // TODO
public static final int BUTTON2_DOWN = 7;
public static final int BUTTON2_DRAG = 8;
public static final int BUTTON2_UP = 9;
public static final int BUTTON2_DOUBLE_CLICK = 10; // TODO
public static final int BUTTON3_DOWN = 11;
public static final int BUTTON3_DRAG = 12;
public static final int BUTTON3_UP = 13;
public static final int BUTTON3_DOUBLE_CLICK = 14; // TODO
public static final int FOCUS_IN = 15;
public static final int FOCUS_OUT = 16;
private final GCanvas canvas_;
private int width_;
private int height_;
private GInteraction interaction_;
private List scenes_;
private GScene interactionScene_;
private Region damageRegion_;
/**
* Create a new graphic window with the specified background color.
* <p>
* The window contains a JComponent canvas which should be added
* to a container widget in the GUI.
*/
public GWindow (Color backgroundColor)
{
// Rendering engine
canvas_ = new GCanvas (this);
if (backgroundColor != null)
canvas_.setBackground (backgroundColor);
interaction_ = null;
scenes_ = new ArrayList();
damageRegion_ = new Region();
// Cannot set 0 initially as resize is computed relative to current
width_ = 100;
height_ = 100;
}
/**
* Create a new graphics window with default background color.
*/
public GWindow()
{
this (null);
}
/**
* Return rendering canvas of this window. This is the component that
* should be added to the client GUI hierarchy.
*
* @return Rendering canvas of this window.
*/
public Component getCanvas()
{
return canvas_;
}
/**
* Return width of this window.
*
* @return Width of this window.
*/
public int getWidth()
{
return width_;
}
/**
* Return height of this window.
*
* @return Height of this window.
*/
public int getHeight()
{
return height_;
}
/**
* Return the current interaction of this window.
*
* @return Current interaction of this window (or null if none installed).
*/
GInteraction getInteraction()
{
return interaction_;
}
/**
* Add a scene to this window. A window may have more than one scene.
* The first scene added is rendered first (i.e. it appears in the
* background of the screen) and so on.
*
* @param scene Scene to add.
*/
void addScene (GScene scene)
{
scenes_.add (scene);
}
/**
* Return all scenes of this window. If no scenes are attached to this
* window, an empty (non-null) list is returned.
*
* @return All scenes of this window.
*/
public List getScenes()
{
return scenes_;
}
/**
* Return the first scene of this window (or null if no scenes are
* attached to this window). This method is a convenience where the
* client application knows that are exactly one scene in the window
* (which in many practical cases will be the case).
*
* @return The first scene of this window (or null if none).
*/
public GScene getScene()
{
return scenes_.size() > 0 ? (GScene) scenes_.get (0) : null;
}
/**
* Find scene at the specified location. If there are more than one scene
* at the specified location, select the front most.
*
* @param x X coordinate of location of scene.
* @param y Y coordinate of location of scene.
* @return Front most scene at specfied location (or null if none).
*/
private GScene getScene (int x, int y)
{
for (int i = scenes_.size()-1; i >= 0; i--) {
GScene scene = (GScene) scenes_.get (i);
GViewport viewport = scene.getViewport();
if (Geometry.isPointInsidePolygon (new int[] {viewport.getX0(),
viewport.getX1(),
viewport.getX3(),
viewport.getX2()},
new int[] {viewport.getY0(),
viewport.getY1(),
viewport.getY3(),
viewport.getY2()},
x, y))
return scene;
}
return null;
}
/**
* Find a GObject based on specified name. Search depth first.
*
* @param name Name of object to search for.
* @return First object with matching name, or null if none found.
*/
public GObject find (String name)
{
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
GObject object = scene.find (name);
if (object != null) return object;
}
return null;
}
/**
* Find a GObject based on user data. Search depth first.
*
* @param name User data of object to search for.
* @return First object with matching user data, or null if none found.
*/
public GObject find (Object userData)
{
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
GObject object = scene.find (userData);
if (object != null) return object;
}
return null;
}
/**
* Return region of damage since last refresh.
*
* @return Damages region since last refresh.
*/
Region getDamageRegion()
{
return damageRegion_;
}
/**
* Add the specified region to the current damage region.
*
* @param region Region to add to damage.
*/
void updateDamageArea (Region region)
{
damageRegion_.union (region);
// It doesn't really matter if the damage region is larger than
// the actual damage, but we'd like to keep it as small as possible
// so we affect as few objects as possible during redraw.
// However, this come as a tradeof with region complexity, and if it
// becomes to complex we choose to "callapse" it, i.e. exchange it
// with its outline extent.
if (damageRegion_.getNRectangles() > 100)
damageRegion_.collapse();
}
/**
* Add the specified rectangle to the current damage region.
*
* @param rectangle Rectangle to add to damage.
*/
void updateDamageArea (Rect rectangle)
{
updateDamageArea (new Region (rectangle));
}
/**
* Install the specified interaction on this window. As a window
* can administrate only one interaction at the time, the current
* interaction (if any) is first stopped.
*
* @param interaction Interaction to install and start.
*/
public void startInteraction (GInteraction interaction)
{
if (interaction_ != null)
stopInteraction();
interaction_ = interaction;
interactionScene_ = null;
}
/**
* Stop the current interaction. The current interaction will get
* an ABORT event so it has the possibility to do cleanup. If no
* interaction is installed, this method has no effect.
*/
public void stopInteraction()
{
// Nothing to do if no current interaction
if (interaction_ == null) return;
interaction_.event (null, ABORT, 0, 0);
interaction_ = null;
interactionScene_ = null;
}
/**
* Ensure correct regions for all objects. Only objects with its
* isRegionValid_ flag set to false (and their parents) will be
* recomputed.
*/
void computeRegion()
{
// This is default setting from window point of view
int visibilityMask = GObject.DATA_VISIBLE |
GObject.ANNOTATION_VISIBLE |
GObject.SYMBOLS_VISIBLE;
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
scene.computeRegion (visibilityMask);
}
}
/**
* Force a complete redraw of all visible elements.
* <p>
* Normally this method is called automatically when needed
* (typically on retransformations).
* A client application <em>may</em> call this method explicitly
* if some external factor that influence the graphics has been
* changed. However, beware of the performance overhead of such
* an approach, and consider calling GObject.redraw() on the
* affected objects instead.
*/
public void redraw()
{
// This is default setting from window point of view
int visibilityMask = GObject.DATA_VISIBLE |
GObject.ANNOTATION_VISIBLE |
GObject.SYMBOLS_VISIBLE |
GObject.WIDGETS_VISIBLE;
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
scene.redraw (visibilityMask);
}
}
/**
* Refresh the graphics scene. Only elements that has been changed
* since the last refresh are affected.
*/
public void refresh()
{
// This is default setting from window point of view
int visibilityMask = GObject.DATA_VISIBLE |
GObject.ANNOTATION_VISIBLE |
GObject.SYMBOLS_VISIBLE |
GObject.WIDGETS_VISIBLE;
// Check if annotation has changed
boolean isAnnotationUpdated = false;
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
if (!scene.isAnnotationValid()) {
isAnnotationUpdated = true;
break;
}
}
// Return here if nothing has changed
if (!isAnnotationUpdated && damageRegion_.isEmpty())
return;
// Compute positions of all annotations
computeTextPositions();
// Compute positions of all integrated AWT components
computeComponentPositions();
// Compute region for all elements
computeRegion();
// Clip damage to viewport
Region viewportRegion = new Region();
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
viewportRegion.union (scene.getRegion());
}
damageRegion_.intersect (viewportRegion);
// Clear the damaged area in the canvas
canvas_.setClipArea (damageRegion_);
canvas_.clear (damageRegion_.getExtent());
Region allDamage = new Region (damageRegion_);
// Rendering pass 1: DATA
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
damageRegion_ = Region.intersect (allDamage, scene.getRegion());
canvas_.setClipArea (damageRegion_);
scene.refreshData (visibilityMask);
}
// Rendering pass 2: ANNOTATION
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
damageRegion_ = Region.intersect (allDamage, scene.getRegion());
canvas_.setClipArea (damageRegion_);
scene.refreshAnnotation (visibilityMask);
}
// Rendering pass 3: COMPONENTS
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
damageRegion_ = Region.intersect (allDamage, scene.getRegion());
canvas_.setClipArea (damageRegion_);
scene.refreshComponents (visibilityMask);
}
canvas_.refresh();
damageRegion_.clear();
}
/**
* Compute all text positions in entire window.
*/
void computeTextPositions()
{
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
if (!scene.isAnnotationValid())
scene.computeTextPositions();
}
}
/**
* Compute all component (Swing widgets) positions in entire window.
*/
void computeComponentPositions()
{
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
// TODO: if (!scene.isAnnotationValid())
scene.computeComponentPositions();
}
}
/**
* Method called when the pointer enters this window. If an interaction
* is installed, pass a FOCUS_IN event to it.
*
* @param x X position of mouse.
* @param y Y position of mouse.
*/
void mouseEntered (int x, int y)
{
if (interaction_ == null) return;
interaction_.event (getScene (x, y), FOCUS_IN, x, y);
}
/**
* Method called when the pointer exits this window. If an interaction
* is installed, pass a FOCUS_OUT event to it.
*
* @param x X position of mouse.
* @param y Y position of mouse.
*/
void mouseExited (int x, int y)
{
if (interaction_ == null) return;
interaction_.event (getScene (x, y), FOCUS_OUT, x, y);
}
/**
* Method called when a mouse pressed event occurs in this window.
* If an interaction is installed, pass a BUTTON*_DOWN event to it.
*
* @param buttonEvent Button event trigging this method.
* @param x X position of mouse.
* @param y Y position of mouse.
*/
void mousePressed (int buttonEvent, int x, int y)
{
if (interaction_ == null) return;
interactionScene_ = getScene (x, y);
interaction_.event (interactionScene_, buttonEvent, x, y);
}
/**
* Method called when a mouse release event occurs in this window.
* If an interaction is installed, pass a BUTTON*_UP event to it.
*
* @param buttonEvent Button event trigging this method.
* @param x X position of mouse.
* @param y Y position of mouse.
*/
void mouseReleased (int buttonEvent, int x, int y)
{
if (interaction_ == null) return;
interaction_.event (interactionScene_, buttonEvent, x, y);
}
/**
* Method called when the mouse is dragged (moved with button pressed) in
* this window. If an interaction is installed, pass a BUTTON*_DRAG
* event to it.
*
* @param buttonEvent Button event trigging this method.
* @param x X position of mouse.
* @param y Y position of mouse.
*/
void mouseDragged (int buttonEvent, int x, int y)
{
if (interaction_ == null) return;
interaction_.event (interactionScene_, buttonEvent, x, y);
}
/**
* Method called when the mouse is moved inside this window.
* If an interaction is installed, pass a MOTION event to it.
*
* @param x X position of mouse.
* @param y Y position of mouse.
*/
void mouseMoved (int x, int y)
{
if (interaction_ == null) return;
interaction_.event (getScene (x, y), MOTION, x, y);
}
/**
* Called when the window is resized. Reset the dimension variables
* and resize scenes accordingly.
*/
void resize()
{
// Get the new window size
int width = canvas_.getWidth();
int height = canvas_.getHeight();
// Refuse to resize to zero as we cannot possible resize back
if (width == 0 || height == 0) return;
// Compute resize factors
double dx = (double) width / (double) width_;
double dy = (double) height / (double) height_;
// Set new window size
width_ = width;
height_ = height;
// Mark entire window as damaged
damageRegion_.clear();
Rect allWindow = new Rect (0, 0, width_, height_);
damageRegion_.union (allWindow);
// Resize every scene accordingly
for (Iterator i = scenes_.iterator(); i.hasNext(); ) {
GScene scene = (GScene) i.next();
scene.resize (dx, dy);
}
// Recompute geometry
redraw();
// Render graphics
refresh();
}
/**
* Print the current image.
*
* @return True if no exception was caught, false otherwise.
*/
public boolean print()
{
boolean isOk = canvas_.print();
return isOk;
}
/**
* Store the current graphic image as a GIF file.
*
* @param file File to store in.
*/
public void saveAsGif (File file)
throws IOException
{
canvas_.saveAsGif (file);
}
/**
* Store the current graphic image as a JPG file.
*
* @param file File to store in.
*/
public void saveAsJpg (File file)
throws IOException
{
canvas_.save (file, "jpg");
}
/**
* Store the current graphic image as a PNG file.
*
* @param file File to store in.
*/
public void saveAsPng (File file)
throws IOException
{
canvas_.save (file, "png");
}
}