/*
* @(#)MiniMapView.java
*
* Project: JHotdraw - a GUI framework for technical drawings
* http://www.jhotdraw.org
* http://jhotdraw.sourceforge.net
* Copyright: (c) by the original author(s) and all contributors
* License: Lesser GNU Public License (LGPL)
* http://www.opensource.org/licenses/lgpl-license.html
*/
package org.jhotdraw.contrib;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.jhotdraw.framework.*;
/**
* Utility component for enhancing component scrolling. It provides a "minature" view of the entire contents of a JScrollPane
* and the means to scroll that JScrollPane by moving a rectangle representing the viewable area of the JScrollPane. If the
* user clicks on an area inside of the MiniMapView, the JScrollPane's view is centered on that area. The user may also drag
* the rectangle representation of the JScrollPane's view around the MiniMap to scroll.
*
* @author S. Ruman (sruman@rogers.com)
* @version <$CURRENT_VERSION$>
*/
public class MiniMapView extends JComponent {
// Instance Variables
private JScrollPane m_subject;
private DrawingView myMappedDrawingView;
private SubjectListener m_subjectListener;
private DrawingChangeListener myDrawingChangeListener;
private Color m_viewBoxColor = Color.red;
private AffineTransform originalTransform;
// Constructors
public MiniMapView(DrawingView newMappedDrawingView, JScrollPane subject) {
m_subjectListener = new SubjectListener();
setSubject(subject);
setMappedDrawingView(newMappedDrawingView);
myDrawingChangeListener = new MappedDrawingChangeListener();
getMappedDrawingView().drawing().addDrawingChangeListener(myDrawingChangeListener);
MouseListener ml = new MouseListener();
addMouseListener(new MouseListener());
addMouseMotionListener(ml);
}
protected void setMappedDrawingView(DrawingView newMappedDrawingView) {
myMappedDrawingView = newMappedDrawingView;
}
public DrawingView getMappedDrawingView() {
return myMappedDrawingView;
}
// accessors
protected void setSubject(JScrollPane subject) {
if (m_subject != null) {
m_subject.getViewport().removeChangeListener(m_subjectListener);
}
m_subject = subject;
if (m_subject != null) {
m_subject.getViewport().addChangeListener(m_subjectListener);
}
repaint();
}
public JScrollPane getSubject() {
return m_subject;
}
public Color getViewBowColor() {
return m_viewBoxColor;
}
public void setViewBoxColor(Color c) {
m_viewBoxColor = c;
repaint();
}
/**
* @return The component that is actually being "mini-mapped", that is the component inside the scroll pane
*/
protected Component getMappedComponent() {
return (Component)getMappedDrawingView();
}
// Overridden
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
/*
* JP, 23-Jan-04: Save original transformation.
*/
originalTransform = g2d.getTransform();
// Paint a small map representation of the subjects contents
Component mappedComponent = getMappedComponent();
AffineTransform at = getViewToMiniMapTransform(mappedComponent);
g2d.transform(at);
getMappedDrawingView().drawAll(g2d);
// Draw a rectangle representing the viewable area
drawViewRectangle(g2d, getViewRectangle());
}
// side-effect of setting the transform on g2d to identity
protected void drawViewRectangle(Graphics2D g2d, Rectangle viewPortRectangle) {
//AffineTransform at = new AffineTransform();
//at.setToIdentity();
//g2d.setTransform(at);
/*
* JP, 23-Jan-04: Restored original transformation instead of setting
* another one. Solves problems with rectangle being outside minimap
* initially. Seems also to conform to JDK specs which say that you must
* never use setTransform() for anything but restoring the original
* transformation.
*/
g2d.setTransform(originalTransform);
g2d.setColor(m_viewBoxColor);
g2d.draw(viewPortRectangle);
}
protected AffineTransform getViewToMiniMapTransform(Component mappedComponent) {
double scaleX = ((double)getWidth()) / ((double)mappedComponent.getWidth());
double scaleY = ((double)getHeight()) / ((double)mappedComponent.getHeight());
AffineTransform at = getInverseSubjectTransform(); // for subclass flexibility
at.concatenate(AffineTransform.getScaleInstance(scaleX, scaleY));
return at;
}
/**
* Allows subclasses to modify the transformation used in creating the mini-map
*/
protected AffineTransform getInverseSubjectTransform() {
AffineTransform at = new AffineTransform();
at.setToIdentity();
return at;
}
/**
* @return The rectangle (in Mini-Map world-coordinates) representing the area being viewed inside of the scroll pane subject
*/
protected Rectangle getViewRectangle() {
Rectangle visiblePortion = m_subject.getViewportBorderBounds();
Point upperLeftViewPos = m_subject.getViewport().getViewPosition();
double [] srcRecCorners = new double[4];
double [] dstRecCorners = new double[4];
srcRecCorners[0] = upperLeftViewPos.x + visiblePortion.getX(); srcRecCorners[1] = upperLeftViewPos.y + visiblePortion.getY(); srcRecCorners[2] = upperLeftViewPos.x + visiblePortion.getX() + visiblePortion.getWidth(); srcRecCorners[3] = upperLeftViewPos.y + visiblePortion.getY() + visiblePortion.getHeight();
getViewToMiniMapTransform(getMappedComponent()).transform(srcRecCorners, 0, dstRecCorners, 0, srcRecCorners.length/2); // transform the coordinates to MiniMapView coordinates
return new Rectangle((int)dstRecCorners[0], (int)dstRecCorners[1], (int)(dstRecCorners[2] - dstRecCorners[0]), (int)(dstRecCorners[3] - dstRecCorners[1]));
}
/**
* Scrolls the subject scroll pane to the coordinates specified.
* @param upperLeftX The new upper left corner X-coordinate (in subject world-coordinates) of the subject scroll pane
* @param upperLeftY The new upper left corner Y-coordinate (in subject world-coordinates) of the subject scroll pane
*/
protected void scrollSubjectTo(int upperLeftX, int upperLeftY) {
AffineTransform at = null;
try {
at = getViewToMiniMapTransform(getMappedComponent()).createInverse();
}
catch (NoninvertibleTransformException nite) {
nite.printStackTrace();
return;
}
double [] srcPoints = new double[2];
double [] destPoints = new double[2];
srcPoints[0] = upperLeftX;
srcPoints[1] = upperLeftY;
at.transform(srcPoints, 0, destPoints, 0, 1);
if (destPoints[0] < 0) {
destPoints[0] = 0;
}
if (destPoints[1] < 0) {
destPoints[1] = 0;
}
m_subject.getViewport().setViewPosition(new Point((int)destPoints[0], (int)destPoints[1]));
}
protected int [] getUpperLeftPointsFromCenter(int centerX, int centerY) {
int [] upperLeft = new int[2];
Rectangle oldRectangle = getViewRectangle();
upperLeft[0] = centerX - oldRectangle.width/2;
upperLeft[1] = centerY - oldRectangle.height/2;
/*
* JP, 25-May-03: Avoid positioning of the rectangle outside the
* available area. Resulted in very strange artifacts on the screen.
*
* JP, 23-Jan-04: Some additional adjustments to ensure that rectangle
* is positioned properly.
*/
if (upperLeft[0] + oldRectangle.width >= getX() + getWidth()) {
upperLeft[0] = getX() + getWidth() - oldRectangle.width - 1;
}
if (upperLeft[1] + oldRectangle.height >= getY() + getHeight()) {
upperLeft[1] = getY() + getHeight() - oldRectangle.height - 1;
}
return upperLeft;
}
// Inner Classes
public class MouseListener extends MouseAdapter implements MouseMotionListener {
public void mousePressed(MouseEvent e) {
int [] rectangleUpperLeft = getUpperLeftPointsFromCenter(e.getX(), e.getY());
scrollSubjectTo(rectangleUpperLeft[0], rectangleUpperLeft[1]);
}
public void mouseDragged(MouseEvent e) {
int [] rectangleUpperLeft = getUpperLeftPointsFromCenter(e.getX(), e.getY());
scrollSubjectTo(rectangleUpperLeft[0], rectangleUpperLeft[1]);
}
public void mouseMoved(MouseEvent e) {
// empty implementation
}
}
class SubjectListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
repaint();
}
}
class MappedDrawingChangeListener implements DrawingChangeListener {
/**
* Sent when an area is invalid
*/
public void drawingInvalidated(DrawingChangeEvent e) {
repaint();
}
/**
* Sent when the drawing wants to be refreshed
*/
public void drawingRequestUpdate(DrawingChangeEvent e) {
repaint();
}
/**
* Sent when the drawing Title has changed
*/
public void drawingTitleChanged(DrawingChangeEvent e) {
}
}
}