package edu.hws.eck.mdb;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* A MandelbrotPanel contains a MandelbrotDisplay and a status bar. The display
* computes and displays a visualization of the Mandelbrot Set. The status bar is
* a JLabel that is used to display information that the user might be interested
* in. A mouse listener is installed on the display that enables the user to
* zoom in and out on the image. Nothing is done to stop the user from zooming
* in beyond the limited accuracy of numbers of type double -- when this happens,
* the picture will first become "blocky" and with even further zooms will become
* meaningless.
*
*
Mouse actions on the display:
*
* - Moving or dragging the mouse shows image coordinates corresponding to mouse location.
*
- Clicking the mouse zooms in on or out from a point. If shift or meta is down when the
* mouse was pressed, or if the right mouse button is used, then zoom out by a factor of 2;
* otherwise, zoom in by a factor of 2. If alt is down when the mouse is pressed, or if
* the middle mouse button is used, then the point that was clicked is moved to the center
* of the image; otherwise, it stays where it is and the rest of the picture moves toward
* that point or away from it.
*
- Dragging the mouse will draw a box around a region of the image. The box is forced
* to have the same aspect ratio (shape) as the display. The box is not drawn if it would
* be too narrow. If the box is there when the mouse is released, then the image is
* zoomed. With no modifier keys and using the left mouse button, the inside of the
* box is zoomed to fill the entire display (this zooms in on the image, which is usually
* what you want). If the shift or meta key is down, or if the right mouse button is used,
* then the entire display is shrunk down into the box (this zooms out).
*
*/
public class MandelbrotPanel extends JPanel {
private MandelbrotDisplay display;
private JLabel statusBar;
/**
* Create a panel containing a MandelbrotDisplay and the label that
* is used as a status bar. Also add a mouse listener of type
* MouseHandler (defined in this class). A ComponentListener
* is added that will respond to a change in the size of the
* display by reporting the new size in the status bar.
*/
public MandelbrotPanel() {
setLayout(new BorderLayout());
display = new MandelbrotDisplay();
statusBar = new JLabel(I18n.tr("Idle"));
add(display,BorderLayout.CENTER);
add(statusBar,BorderLayout.SOUTH);
MouseHandler mouser = new MouseHandler();
display.addMouseListener(mouser);
display.addMouseMotionListener(mouser);
display.addComponentListener( new ComponentAdapter() {
public void componentResized(ComponentEvent evt) {
// Called when the display changes size; report the new
// size in the status bar. Note that this is also
// called when the display gets its initial size, so
// when the program opens, the image size will be shown
// in the status bar.
String w = "" + display.getWidth();
String h = "" + display.getHeight();
statusBar.setText(I18n.tr("status.imageSize",w,h));
}
});
}
/**
* Returns the MandelbrotDisplay that is contained in this panel.
*/
public MandelbrotDisplay getDisplay() {
return display;
}
/**
* Zoom in on or out from a point in the image.
* @param x x-coordinate of the point at the center of the zoom
* @param y y-coordinate of the point at the center of the zoom
* @param factor magnification or shrinking factor. If factor is
* greater than 1, zoom out. If factor is less than 1, zoom in.
* For example, factor=0.5 shrinks the x,y ranges in the image
* to half their previous size.
* @param movePointToCenter if true, then the image point at pixel
* position (x,y) is moved to the center pixel of the image after
* the zoom; if false, the point is not moved so that the pixel
* at (x,y) represents the same point after the zoom as before
* and all the other points move towards or away from that one.
*/
public void zoom(int x, int y, double factor, boolean movePointToCenter) {
double xmin = display.getXmin();
double xmax = display.getXmax();
double ymin = display.getYmin();
double ymax = display.getYmax();
double newWidth = factor*(xmax-xmin);
double newHeight = factor*(ymax-ymin);
double centerX = xmin + ((double)x)/display.getWidth()*(xmax-xmin);
double centerY = ymax - ((double)y)/display.getHeight()*(ymax-ymin);
if (movePointToCenter) {
display.setLimits(centerX-newWidth/2,centerX+newWidth/2,
centerY-newHeight/2,centerY+newHeight/2);
}
else {
double newXmin = centerX - newWidth*(centerX-xmin)/(xmax-xmin);
double newYmin = centerY - newHeight*(centerY-ymin)/(ymax-ymin);
display.setLimits(newXmin,newXmin+newWidth,newYmin,newYmin+newHeight);
}
}
/**
* Display the coordinates of the image point that corresponds
* to pixel coordinates (x,y).
*/
private void doShowCoordsInStatusBar(int x, int y) {
double xmin = display.getXmin();
double xmax = display.getXmax();
double ymin = display.getYmin();
double ymax = display.getYmax();
double width = display.getWidth();
double height = display.getHeight();
double xCoord = xmin + x/width*(xmax-xmin);
double yCoord = ymax - y/height*(ymax-ymin);
// The next 10 lines try to avoid more digits after the decimal
// points than makes sense. If it succeeds the coordinates
// that are shown should differ only in their last few digits.
double diff = xmax - xmin;
int scale = 4;
if (diff > 0) {
while (diff < 1) {
scale++;
diff *= 10;
}
}
String xStr = String.format("%1." + scale + "f", xCoord);
String yStr = String.format("%1." + scale + "f", yCoord);
statusBar.setText(I18n.tr("status.mouseCoords",xStr,yStr));
}
/**
* Defines the listener that responds to user mouse actions on the display.
* Note that the (x,y) coordinates for the events refer to the display, since
* the listeners are registered to respond to events on the display, not on
* this panel. Mouse drags and clicks are used for zooming the image. Dragging
* and mouse motion also show the current mouse coordinates in the status bar.
*/
private class MouseHandler implements MouseListener, MouseMotionListener {
int startX, startY; // Location of mousePressed event.
boolean dragging; // True if a drag operation is in progress.
boolean zoomOut; // True if the action will be a zoom out rather than
// a zoom in. This is set to true if the shift key
// or meta key is down for the mousePressed action.
// (Also true if the right-mouse button is used.)
boolean moved; // During a drag operation, this becomes true if
// the mouse actually moves at least a few pixels.
// If so, the mouse action is interpreted as a
// click rather than a drag.
boolean movePointToCenter; // True if the click point for a click
// operation should be moved to the center
// of the image; false if it should not
// be moved. This is set in the mousePressed
// routine to be true if the alt/option key
// is down (or the middle mouse button is used).
public void mousePressed(MouseEvent evt) {
doShowCoordsInStatusBar(startX,startY);
dragging = false;
if (display.getStatus() == MandelbrotDisplay.STATUS_OUT_OF_MEMORY) {
// If there is not enough memory to have an image in the display,
// don't try to zoom it!
return;
}
startX = evt.getX();
startY = evt.getY();
zoomOut = evt.isShiftDown() || evt.isMetaDown();
dragging = true;
moved = false;
movePointToCenter = evt.isAltDown();
}
public void mouseReleased(MouseEvent evt) {
if (!dragging)
return;
if (moved) // If moved is true, this is a drag operation, otherwise, a click.
display.applyZoom(zoomOut); // zoom into or out of zoom rect that user has drawn
else if (zoomOut)
zoom(startX,startY,2,movePointToCenter); // zoom out from point
else
zoom(startX,startY,0.5,movePointToCenter); // zoom in on point
dragging = false;
}
public void mouseDragged(MouseEvent evt) {
int x = evt.getX();
int y = evt.getY();
doShowCoordsInStatusBar(x,y);
if (!dragging)
return;
int width = Math.abs(x-startX);
int height = Math.abs(y-startY);
if (Math.abs(width) < 3 || Math.abs(height) < 3) {
// Too close to start point to have a zoom box.
if (!display.drawZoomBox(null)) {
// display.drawZoomBox() returns false if there is
// some error, such as not having an image, that
// prevents having a zoom box. This is unlikely to
// happen, but if it does, we should cancel the zoom.
dragging = false;
}
return;
}
moved = true; // Mouse has moved more than 2 pixels away from start position.
// During a draw operation, a zoom box is drawn with one corner at the
// mouse's starting position. When the mouse is released, the image
// is zoomed out form this box or into this box.
// The next 6 lines adjust the shape of the zoom box so that it matches
// the shape of the window. This is so that zooming will use the same
// magnification factor in both directions.
double aspect = (double)width/height;
double imageAspect = (double)display.getWidth()/display.getHeight();
if (aspect < imageAspect)
width = (int)(width*imageAspect/aspect+0.49);
else if (aspect > imageAspect)
height = (int)(height*aspect/imageAspect+0.49);
// The next 9 lines compute the upper left corner of the rectangle,
// so that it has one corner at the start position of the mouse;
// width and height represent the size of the zoom rect.
int x1,y1;
if (x < startX)
x1 = startX - width;
else
x1 = startX;
if (y < startY)
y1 = startY - height;
else
y1 = startY;
Rectangle rect = new Rectangle(x1,y1,width,height);
if (!display.drawZoomBox(rect))
dragging = false;
}
public void mouseMoved(MouseEvent evt) {
doShowCoordsInStatusBar(evt.getX(),evt.getY());
}
public void mouseExited(MouseEvent evt) {
// When mouse moves out of the display, get rid of the coordinate
// display in the status bar.
statusBar.setText("Idle");
}
public void mouseEntered(MouseEvent evt) { }
public void mouseClicked(MouseEvent evt) { }
} // end nested class MouseHandler
} // end class MandelbrotPanel