/*
* (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.HashMap;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.LayoutManager;
import java.awt.Paint;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.font.TextLayout;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import javax.swing.JComponent;
import javax.swing.RepaintManager;
import javax.imageio.ImageIO;
import no.geosoft.cc.geometry.Rect;
import no.geosoft.cc.geometry.Region;
import no.geosoft.cc.io.GifEncoder;
/**
* G rendering engine.
* <p>
* The canvas is the AWT component where the graphics is displayed.
*
* @author <a href="mailto:jacob.dreyer@geosoft.no">Jacob Dreyer</a>
*/
class GCanvas extends JComponent
implements Printable, LayoutManager, MouseListener, MouseMotionListener,
ComponentListener
{
private final GWindow window_;
private final RepaintManager repaintManager_;
private Graphics graphics_;
private Image backBuffer_;
private Area clipArea_;
private Rect cleared_;
/**
* Create a graphic canvas.
*
* @param window GWindow of this canvas.
*/
GCanvas (GWindow window)
{
window_ = window;
setLayout (this);
repaintManager_ = RepaintManager.currentManager (this);
// We handle double buffering manually
repaintManager_.setDoubleBufferingEnabled (false);
backBuffer_ = null;
clipArea_ = null;
cleared_ = null;
addMouseListener (this);
addMouseMotionListener (this);
addComponentListener (this);
}
/**
* Override the JPanel default to indicate that this canvas is
* double buffered.
*
* @return True always.
*/
public boolean isDoubleBuffered()
{
return true;
}
/**
* Override the JPanel default repaint method and do nothing.
* TODO: Check this.
*/
public void repaint()
{
}
/**
* Override the JPanel update method and do nothing.
* TODO: Check this.
*
* @param graphics The Graphics2D instance.
*/
public void update (Graphics graphics)
{
}
/**
* Create a new image back buffer. The back buffes has the size
* of the cnvas and is recreated each time the canvas size
* changes (on resize events).
*/
private void createBackBuffer()
{
int width = getWidth();
int height = getHeight();
if (width <= 0 || height <= 0) return;
// If component has no parent, null is returned
backBuffer_ = createImage (width, height);
// Fill back buffer with background color
if (backBuffer_ != null) {
Graphics2D canvas = (Graphics2D) backBuffer_.getGraphics();
canvas.setColor (getBackground());
canvas.fillRect (0, 0, width, height);
}
}
/**
* Paint this component by copying the back buffer into the
* front buffer.
*
* @param graphics The Java2D graphics instance.
*/
public void paintComponent (Graphics graphics)
{
if (backBuffer_ == null || graphics == null || cleared_ == null)
return;
graphics_ = graphics;
// TODO: Is it possible to save the rect from clear() and
// only copy this part??
// Copy the current back buffer to the front buffer
Graphics2D frontBuffer = (Graphics2D) graphics_;
frontBuffer.drawImage (backBuffer_,
cleared_.x, cleared_.y,
cleared_.width, cleared_.height,
cleared_.x, cleared_.y,
cleared_.width, cleared_.height,
this);
// TODO: These work as well. Which method is better?
// frontBuffer.drawImage (backBuffer_, 0, 0, getWidth(), getHeight(), this);
// frontBuffer.drawImage (backBuffer_, null, this);
// frontBuffer.drawImage (backBuffer_, 0, 0, this);
// Due to a bug in the repaint manager
// TODO: Check this
if (repaintManager_.getDirtyRegion (this).isEmpty())
return;
// Mark as valid
repaintManager_.paintDirtyRegions();
repaintManager_.markCompletelyClean (this);
cleared_ = null;
}
/**
* Clear the specified area in the back buffer.
*
* @param rectangle Rectangle area to clear in the back buffer.
*/
void clear (Rect rectangle)
{
if (graphics_ == null || backBuffer_ == null)
return;
Graphics2D canvas = (Graphics2D) backBuffer_.getGraphics();
// Clear by filling rectangle with background color
canvas.setClip (clipArea_);
canvas.setColor (getBackground());
canvas.fillRect (rectangle.x, rectangle.y,
rectangle.width, rectangle.height);
}
/**
* Refresh this canvas.
*/
void refresh()
{
paintComponent (getGraphics());
}
/**
* Set clip area for upcomming draw operations.
*
* @param region Region to use as clip area.
*/
void setClipArea (Region region)
{
clipArea_ = region == null || region.isEmpty() ? null : region.createArea();
}
/**
* Render the specified polyline into back buffer using the
* specified style.
*
* @param x Polyline x coordinates.
* @param y Polyline y coordinates.
* @param style Style used for rendering.
*/
void render (int x[], int y[], GStyle style)
{
if (backBuffer_ == null)
createBackBuffer();
if (backBuffer_ == null)
return;
Graphics2D canvas = (Graphics2D) backBuffer_.getGraphics();
canvas.setRenderingHint (RenderingHints.KEY_ANTIALIASING,
style.isAntialiased() ?
RenderingHints.VALUE_ANTIALIAS_ON :
RenderingHints.VALUE_ANTIALIAS_OFF);
canvas.setColor (style.getForegroundColor());
canvas.setStroke (style.getStroke());
canvas.setClip (clipArea_);
Paint paint = style.getPaint();
if (paint != null) {
Paint defaultPaint = canvas.getPaint();
canvas.setPaint (paint);
canvas.fill (new Polygon (x, y, x.length));
canvas.setPaint (defaultPaint);
if (style.isLineVisible())
canvas.drawPolyline (x, y, x.length);
}
else {
if (style.isLineVisible())
canvas.drawPolyline (x, y, x.length);
}
}
/**
* Render the specified text element into back buffer using the
* specified style.
*
* @param text Text to render.
* @param style Style used for rendering.
*/
void render (GText text, GStyle style)
{
if (backBuffer_ == null) return;
String string = text.getText();
if (string == null || string.length() == 0) return;
Graphics2D canvas = (Graphics2D) backBuffer_.getGraphics();
canvas.setRenderingHint (RenderingHints.KEY_TEXT_ANTIALIASING,
style.isAntialiased() ?
RenderingHints.VALUE_TEXT_ANTIALIAS_ON :
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
canvas.setFont (style.getFont());
int fontSize = style.getFont().getSize();
Color foregroundColor = style.getForegroundColor();
Color backgroundColor = style.getBackgroundColor();
Rect rectangle = text.getRectangle();
// Draw box
if (backgroundColor != null) {
canvas.setColor (backgroundColor);
canvas.fillRect (rectangle.x, rectangle.y,
rectangle.width, rectangle.height);
}
// Draw text, center it in the rectangle box
canvas.setColor (foregroundColor);
TextLayout layout = new TextLayout (text.getText(), style.getFont(),
canvas.getFontRenderContext());
Rectangle2D bounds = layout.getBounds();
double textWidth = bounds.getWidth();
double textHeight = bounds.getHeight();
int x = rectangle.x +
(int) Math.round ((rectangle.width - textWidth) / 2.0) -
(int) Math.floor (bounds.getX());
int y = rectangle.y +
(int) Math.round ((rectangle.height - textHeight) / 2.0) -
(int) Math.floor (bounds.getY());
layout.draw (canvas, (float) x, (float) y);
}
/**
* Render the specified image into back buffer.
*
* @param image Image to render.
*/
void render (GImage image)
{
Graphics2D canvas = (Graphics2D) backBuffer_.getGraphics();
Rect rectangle = image.getRectangle();
canvas.drawImage (image.getImage(), rectangle.x, rectangle.y, this);
}
/**
* Render the specified image at every vertex along the specified
* polyline.
*
* @param x Polyline x components.
* @param y Polyline y components.
* @param image Image to render.
*/
void render (int[] x, int[] y, GImage image)
{
Graphics2D canvas = (Graphics2D) backBuffer_.getGraphics();
// The image rectangle x,y holds delta values for positioning
int dx = image.getRectangle().x;
int dy = image.getRectangle().y;
int nPoints = x.length;
for (int i = 0; i < nPoints; i++)
canvas.drawImage (image.getImage(), x[i] + dx, y[i] + dy, this);
}
/**
* Position the specified AWT component within this JPanel.
*
* @param component AWT component to position.
*/
void render (GComponent component)
{
Component c = component.getComponent();
c.setLocation (component.getRectangle().x,
component.getRectangle().y);
if (!isAncestorOf (c))
add (c);
}
/**
* Utility method for computing the rectangle bounding box of
* a rendered string using the specified font.
*
* @param string Sample string.
* @param font Font to use.
* @return Rectangle bounding box of rendered string.
*/
Rect getStringBox (String string, Font font)
{
Graphics2D graphics = (Graphics2D) getGraphics();
// For some reason the graphics is not always set
if (graphics == null)
return new Rect (0, 0, 0, 0);
TextLayout textLayout = new TextLayout (string, font,
graphics.getFontRenderContext());
Rectangle2D bounds = textLayout.getBounds();
int width = (int) Math.ceil (bounds.getWidth());
int height = (int) Math.ceil (bounds.getHeight());
return new Rect (0, 0, width, height);
}
/**
* From the Printable interface. Print the present canvas.
*
* @param graphics The paper graphics.
* @param pageFormat Page format (not used).
* @param pageIndex Page index (not used).
* @return Printable.PAGE_EXISTS.
*/
public int print (Graphics graphics, PageFormat pageFormat, int pageIndex)
{
if (pageIndex > 0) return Printable.NO_SUCH_PAGE;
Graphics2D paper = (Graphics2D) graphics;
paper.drawImage (backBuffer_, 0, 0, getWidth(), getHeight(), this);
return Printable.PAGE_EXISTS;
}
/**
* Save the current graphics as a gif file.
*
* @param file File to store in.
*/
void saveAsGif (File file)
throws IOException
{
try {
GifEncoder gifEncoder = new GifEncoder (backBuffer_);
OutputStream stream = new FileOutputStream (file);
gifEncoder.write (stream);
stream.close();
}
catch (AWTException exception) {
throw new IOException ("Failed to save as GIF");
}
}
/**
* Save the current graphics to file.
*
* @param file File to store in.
* @param format File format. (@see ImageIO)
*/
void save (File file, String format)
throws IOException
{
ImageIO.write ((BufferedImage) backBuffer_, format, file);
}
/**
* Print the canvas content.
*
* @return True if no exception was caught, false otherwise.
*/
boolean print()
{
try {
PrinterJob printerJob = PrinterJob.getPrinterJob();
printerJob.setPrintable (this);
printerJob.print();
return true;
}
catch (PrinterException exception) {
return false;
}
}
/**
* Implied by LayoutManager
*
* @param name Name of component to add.
* @param component Component to add.
*/
public void addLayoutComponent (String name, Component component)
{
}
/**
* Implied by LayoutManager. Layout the specified container. All components
* are positined at thir specified x, y location as determined by
* the GAnnotator.
*
* @param container Container to layout.
*/
public void layoutContainer (Container container)
{
Component[] components = container.getComponents();
for (int i = 0; i < components.length; i++) {
Component component = components[i];
int x = component.getX();
int y = component.getY();
int width = component.getPreferredSize().width;
int height = component.getPreferredSize().height;
component.setBounds (x, y, width, height);
}
}
/**
* Implied by LayoutManager. Return minimum layout size.
*
* @param container Component to return minimum layout size of.
* @return Size of canvas window.
*/
public Dimension minimumLayoutSize (Container container)
{
return new Dimension (getWidth(), getHeight());
}
/**
* Implied by LayoutManager. Return maximum layout size.
*
* @param container Component to return maximum layout size of.
* @return Size of canvas window.
*/
public Dimension preferredLayoutSize (Container container)
{
return new Dimension (getWidth(), getHeight());
}
/**
* Implied by LayoutManager.
*
* @param component Component to remove.
*/
public void removeLayoutComponent (Component component)
{
}
/**
* Implied by MouseListener. Not used.
*
* @param event Mouse event trigging this method.
*/
public void mouseClicked (MouseEvent event)
{
}
/**
* Method called when the pointer enters this window. If an interaction
* is installed, pass a FOCUS_IN event to it.
*
* @param event Mouse event trigging this method.
*/
public void mouseEntered (MouseEvent event)
{
window_.mouseEntered (event.getX(), event.getY());
}
/**
* Method called when the pointer exits this window. If an interaction
* is installed, pass a FOCUS_OUT event to it.
*
* @param event Mouse event trigging this method.
*/
public void mouseExited (MouseEvent event)
{
window_.mouseExited (event.getX(), event.getY());
}
/**
* Method called when a mouse pressed event occurs in this window.
* If an interaction is installed, pass a BUTTON*_DOWN event to it.
*
* @param event Mouse event trigging this method.
*/
public void mousePressed (MouseEvent event)
{
int modifiers = event.getModifiers();
int buttonEvent;
if (modifiers == event.BUTTON1_MASK)
buttonEvent = GWindow.BUTTON1_DOWN;
else if (modifiers == event.BUTTON2_MASK)
buttonEvent = GWindow.BUTTON2_DOWN;
else
buttonEvent = GWindow.BUTTON3_DOWN;
window_.mousePressed (buttonEvent, event.getX(), event.getY());
}
/**
* Method called when a mouse release event occurs in this window.
* If an interaction is installed, pass a BUTTON*_UP event to it.
*
* @param event Mouse event trigging this method.
*/
public void mouseReleased (MouseEvent event)
{
int modifiers = event.getModifiers();
int buttonEvent;
if (modifiers == event.BUTTON1_MASK)
buttonEvent = GWindow.BUTTON1_UP;
else if (modifiers == event.BUTTON2_MASK)
buttonEvent = GWindow.BUTTON2_UP;
else
buttonEvent = GWindow.BUTTON3_UP;
window_.mouseReleased (buttonEvent, event.getX(), event.getY());
}
/**
* 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 event Mouse event trigging this method.
*/
public void mouseDragged (MouseEvent event)
{
int modifiers = event.getModifiers();
int buttonEvent;
if (modifiers == event.BUTTON1_MASK)
buttonEvent = GWindow.BUTTON1_DRAG;
else if (modifiers == event.BUTTON2_MASK)
buttonEvent = GWindow.BUTTON2_DRAG;
else
buttonEvent = GWindow.BUTTON3_DRAG;
window_.mouseDragged (buttonEvent, event.getX(), event.getY());
}
/**
* Method called when the mouse is moved inside this window.
* If an interaction is installed, pass a MOTION event to it.
*
* @param event Mouse event trigging this method.
*/
public void mouseMoved (MouseEvent event)
{
window_.mouseMoved (event.getX(), event.getY());
}
/**
* Implied by ComponentListener. Not used.
*
* @param event Event trigging this method.
*/
public void componentHidden (ComponentEvent event) {}
/**
* Implied by ComponentListener. Not used.
*
* @param event Event trigging this method.
*/
public void componentMoved (ComponentEvent event) {}
/**
* Implied by ComponentListener. Not used.
*
* @param event Event trigging this method.
*/
public void componentShown (ComponentEvent event) {}
/**
* Called when the AWT component is resized.
*
* @param event Resize event.
*/
public void componentResized (ComponentEvent event)
{
createBackBuffer();
cleared_ = new Rect (0, 0, getWidth(), getHeight());
window_.resize();
}
}