package edu.oregonstate.cartography.gui;

import edu.oregonstate.cartography.app.ImageUtils;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

/**
 * https://today.java.net/pub/a/today/2007/03/27/navigable-image-panel.html
 */
/**
 * <p>
 * <code>NavigableImagePanel</code> is a lightweight container displaying an
 * image that can be zoomed in and out and panned with ease and simplicity,
 * using an adaptive rendering for high quality display and satisfactory
 * performance.
 * </p>
 * <p>
 * <h3>Image</h3>
 * <p>
 * An image is loaded either via a constructor:</p>
 * <pre>
 * NavigableImagePanel panel = new NavigableImagePanel(image);
 * </pre> or using a setter:
 * <pre>
 * NavigableImagePanel panel = new NavigableImagePanel();
 * panel.setImage(image);
 * </pre> When an image is set, it is initially painted centered in the
 * component, at the largest possible size, fully visible, with its aspect ratio
 * is preserved. This is defined as 100% of the image size and its corresponding
 * zoom level is 1.0.
 * </p>
 * <h3>Zooming</h3>
 * <p>
 * Zooming can be controlled interactively, using either the mouse scroll wheel
 * (default) or the mouse two buttons, or programmatically, allowing the
 * programmer to implement other custom zooming methods. If the mouse does not
 * have a scroll wheel, set the zooming device to mouse buttons:
 * <pre>
 * panel.setZoomDevice(ZoomDevice.MOUSE_BUTTON);
 * </pre> The left mouse button works as a toggle switch between zooming in and
 * zooming out modes, and the right button zooms an image by one increment
 * (default is 20%). You can change the zoom increment value by:
 * <pre>
 * panel.setZoomIncrement(newZoomIncrement);
 * </pre> If you intend to provide programmatic zoom control, set the zoom
 * device to none to disable both the mouse wheel and buttons for zooming
 * purposes:
 * <pre>
 * panel.setZoomDevice(ZoomDevice.NONE);
 * </pre> and use <code>setZoom()</code> to change the zoom level.
 * </p>
 * <p>
 * Zooming is always around the point the mouse pointer is currently at, so that
 * this point (called a zooming center) remains stationary ensuring that the
 * area of an image we are zooming into does not disappear off the screen. The
 * zooming center stays at the same location on the screen and all other points
 * move radially away from it (when zooming in), or towards it (when zooming
 * out). For programmatically controlled zooming the zooming center is either
 * specified when <code>setZoom()</code> is called:
 * <pre>
 * panel.setZoom(newZoomLevel, newZoomingCenter);
 * </pre> or assumed to be the point of an image which is the closest to the
 * center of the panel, if no zooming center is specified:
 * <pre>
 * panel.setZoom(newZoomLevel);
 * </pre>
 * </p>
 * <p>
 * There are no lower or upper zoom level limits.
 * </p>
 * <h3>Navigation</h3>
 * <p>
 * <code>NavigableImagePanel</code> does not use scroll bars for navigation, but
 * relies on a navigation image located in the upper left corner of the panel.
 * The navigation image is a small replica of the image displayed in the panel.
 * When you click on any point of the navigation image that part of the image is
 * displayed in the panel, centered. The navigation image can also be zoomed in
 * the same way as the main image.</p>
 * <p>
 * In order to adjust the position of an image in the panel, it can be dragged
 * with the mouse, using the left button.
 * </p>
 * <p>
 * For programmatic image navigation, disable the navigation image:
 * <pre>
 * panel.setNavigationImageEnabled(false)
 * </pre> and use <code>getImageOrigin()</code> and
 * <code>setImageOrigin()</code> to move the image around the panel.
 * </p>
 * <h3>Rendering</h3>
 * <p>
 * <code>NavigableImagePanel</code> uses the Nearest Neighbor interpolation for
 * image rendering (default in Java). When the scaled image becomes larger than
 * the original image, the Bilinear interpolation is applied, but only to the
 * part of the image which is displayed in the panel. This interpolation change
 * threshold can be controlled by adjusting the value of
 * <code>HIGH_QUALITY_RENDERING_SCALE_THRESHOLD</code>.</p>
 */
public class NavigableImagePanel extends JPanel {

    /**
     * <p>
     * Identifies a change to the zoom level.</p>
     */
    public static final String ZOOM_LEVEL_CHANGED_PROPERTY = "zoomLevel";

    /**
     * <p>
     * Identifies a change to the zoom increment.</p>
     */
    public static final String ZOOM_INCREMENT_CHANGED_PROPERTY = "zoomIncrement";

    /**
     * <p>
     * Identifies that the image in the panel has changed.</p>
     */
    public static final String IMAGE_CHANGED_PROPERTY = "image";

    private static final double SCREEN_NAV_IMAGE_FACTOR = 0.15; // 15% of panel's width
    private static final double NAV_IMAGE_FACTOR = 0.3; // 30% of panel's width
    private static final double HIGH_QUALITY_RENDERING_SCALE_THRESHOLD = 0;
    private static final Object INTERPOLATION_TYPE
            = RenderingHints.VALUE_INTERPOLATION_BILINEAR;

    private double zoomIncrement = 0.2;
    private double zoomFactor = 1.0 + zoomIncrement;
    private double navZoomFactor = 1.0 + zoomIncrement;
    private BufferedImage image;
    private BufferedImage navigationImage;
    private int navImageWidth;
    private int navImageHeight;
    private double initialScale = 0.0;
    private double scale = 0.0;
    private double navScale = 0.0;
    private int originX = 0;
    private int originY = 0;
    private Point mousePosition;
    private Dimension previousPanelSize;
    private boolean navigationImageEnabled = true;
    private boolean highQualityRenderingEnabled = true;

    private WheelZoomDevice wheelZoomDevice = null;
    private ButtonZoomDevice buttonZoomDevice = null;

    /**
     * <p>
     * Defines zoom devices.</p>
     */
    public static class ZoomDevice {

        /**
         * <p>
         * Identifies that the panel does not implement zooming, but the
         * component using the panel does (programmatic zooming method).</p>
         */
        public static final ZoomDevice NONE = new ZoomDevice("none");

        /**
         * <p>
         * Identifies the left and right mouse buttons as the zooming
         * device.</p>
         */
        public static final ZoomDevice MOUSE_BUTTON = new ZoomDevice("mouseButton");

        /**
         * <p>
         * Identifies the mouse scroll wheel as the zooming device.</p>
         */
        public static final ZoomDevice MOUSE_WHEEL = new ZoomDevice("mouseWheel");

        private String zoomDevice;

        private ZoomDevice(String zoomDevice) {
            this.zoomDevice = zoomDevice;
        }

        public String toString() {
            return zoomDevice;
        }
    }

    //This class is required for high precision image coordinates translation.
    private class Coords {

        public double x;
        public double y;

        public Coords(double x, double y) {
            this.x = x;
            this.y = y;
        }

        public int getIntX() {
            return (int) Math.round(x);
        }

        public int getIntY() {
            return (int) Math.round(y);
        }

        public String toString() {
            return "[Coords: x=" + x + ",y=" + y + "]";
        }
    }

    private class WheelZoomDevice implements MouseWheelListener {

        public void mouseWheelMoved(MouseWheelEvent e) {
            Point p = e.getPoint();
            boolean zoomIn = (e.getWheelRotation() < 0);
            if (isInNavigationImage(p)) {
                if (zoomIn) {
                    navZoomFactor = 1.0 + zoomIncrement;
                } else {
                    navZoomFactor = 1.0 - zoomIncrement;
                }
                zoomNavigationImage();
            } else if (isInImage(p)) {
                if (zoomIn) {
                    zoomFactor = 1.0 + zoomIncrement;
                } else {
                    zoomFactor = 1.0 - zoomIncrement;
                }
                zoomImage();
            }
        }
    }

    private class ButtonZoomDevice extends MouseAdapter {

        public void mouseClicked(MouseEvent e) {
            Point p = e.getPoint();
            if (SwingUtilities.isRightMouseButton(e)) {
                if (isInNavigationImage(p)) {
                    navZoomFactor = 1.0 - zoomIncrement;
                    zoomNavigationImage();
                } else if (isInImage(p)) {
                    zoomFactor = 1.0 - zoomIncrement;
                    zoomImage();
                }
            } else {
                if (isInNavigationImage(p)) {
                    navZoomFactor = 1.0 + zoomIncrement;
                    zoomNavigationImage();
                } else if (isInImage(p)) {
                    zoomFactor = 1.0 + zoomIncrement;
                    zoomImage();
                }
            }
        }
    }

    /**
     * <p>
     * Creates a new navigable image panel with no default image and the mouse
     * scroll wheel as the zooming device.</p>
     */
    public NavigableImagePanel() {
        setOpaque(false);
        addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                if (scale > 0.0) {
                    if (isFullImageInPanel()) {
                        centerImage();
                    } else if (isImageEdgeInPanel()) {
                        scaleOrigin();
                    }
                    if (isNavigationImageEnabled()) {
                        createNavigationImage();
                    }
                    repaint();
                }
                previousPanelSize = getSize();
            }
        });

        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                if (SwingUtilities.isLeftMouseButton(e)) {
                    if (isInNavigationImage(e.getPoint())) {
                        Point p = e.getPoint();
                        displayImageAt(p);
                    }
                }
            }

            // FIXME hack
            public void mouseClicked(MouseEvent e) {
                if (SwingUtilities.isRightMouseButton(e)) {
                    double w = 100 * (e.getX() - originX) / scale / image.getWidth();
                    double h = 100 * (e.getY() - originY) / scale / image.getHeight();
                    if (w < 0 || w > 100 || h < 0 || h > 100) {
                        w = h = -1;
                    }
                    firePropertyChange("mouseRightClicked", w, h);
                }
            }
        });

        addMouseMotionListener(new MouseMotionListener() {
            public void mouseDragged(MouseEvent e) {
                if (SwingUtilities.isLeftMouseButton(e)
                        && !isInNavigationImage(e.getPoint())) {
                    Point p = e.getPoint();
                    moveImage(p);
                }
            }

            public void mouseMoved(MouseEvent e) {
                //we need the mouse position so that after zooming
                //that position of the image is maintained
                mousePosition = e.getPoint();

                if (image != null) {
                    double w = 100 * (e.getX() - originX) / scale / image.getWidth();
                    double h = 100 * (e.getY() - originY) / scale / image.getHeight();
                    if (w < 0 || w > 100 || h < 0 || h > 100) {
                        w = h = -1;
                    }
                    firePropertyChange("mouseMoved", w, h);
                }
            }
        });

        setZoomDevice(ZoomDevice.MOUSE_WHEEL);
    }

    /**
     * <p>
     * Creates a new navigable image panel with the specified image and the
     * mouse scroll wheel as the zooming device.</p>
     */
    public NavigableImagePanel(BufferedImage image) throws IOException {
        this();
        setImage(image);
    }

    private void addWheelZoomDevice() {
        if (wheelZoomDevice == null) {
            wheelZoomDevice = new WheelZoomDevice();
            addMouseWheelListener(wheelZoomDevice);
        }
    }

    private void addButtonZoomDevice() {
        if (buttonZoomDevice == null) {
            buttonZoomDevice = new ButtonZoomDevice();
            addMouseListener(buttonZoomDevice);
        }
    }

    private void removeWheelZoomDevice() {
        if (wheelZoomDevice != null) {
            removeMouseWheelListener(wheelZoomDevice);
            wheelZoomDevice = null;
        }
    }

    private void removeButtonZoomDevice() {
        if (buttonZoomDevice != null) {
            removeMouseListener(buttonZoomDevice);
            buttonZoomDevice = null;
        }
    }

    /**
     * <p>
     * Sets a new zoom device.</p>
     *
     * @param newZoomDevice specifies the type of a new zoom device.
     */
    public void setZoomDevice(ZoomDevice newZoomDevice) {
        if (newZoomDevice == ZoomDevice.NONE) {
            removeWheelZoomDevice();
            removeButtonZoomDevice();
        } else if (newZoomDevice == ZoomDevice.MOUSE_BUTTON) {
            removeWheelZoomDevice();
            addButtonZoomDevice();
        } else if (newZoomDevice == ZoomDevice.MOUSE_WHEEL) {
            removeButtonZoomDevice();
            addWheelZoomDevice();
        }
    }

    /**
     * <p>
     * Gets the current zoom device.</p>
     */
    public ZoomDevice getZoomDevice() {
        if (buttonZoomDevice != null) {
            return ZoomDevice.MOUSE_BUTTON;
        } else if (wheelZoomDevice != null) {
            return ZoomDevice.MOUSE_WHEEL;
        } else {
            return ZoomDevice.NONE;
        }
    }

    //Called from paintComponent() when a new image is set.
    private void initializeParams() {
        double xScale = (double) getWidth() / image.getWidth();
        double yScale = (double) getHeight() / image.getHeight();
        initialScale = Math.min(xScale, yScale);
        scale = initialScale;

        //An image is initially centered
        centerImage();
        if (isNavigationImageEnabled()) {
            createNavigationImage();
        }
    }

    //Centers the current image in the panel.
    private void centerImage() {
        originX = (int) (getWidth() - getScreenImageWidth()) / 2;
        originY = (int) (getHeight() - getScreenImageHeight()) / 2;
    }

    //Creates and renders the navigation image in the upper let corner of the panel.
    private void createNavigationImage() {
        //We keep the original navigation image larger than initially 
        //displayed to allow for zooming into it without pixellation effect.
        navImageWidth = (int) (getWidth() * NAV_IMAGE_FACTOR);
        navImageHeight = navImageWidth * image.getHeight() / image.getWidth();
        int scrNavImageWidth = (int) (getWidth() * SCREEN_NAV_IMAGE_FACTOR);
        int scrNavImageHeight = scrNavImageWidth * image.getHeight() / image.getWidth();
        navScale = (double) scrNavImageWidth / navImageWidth;
        navigationImage = new BufferedImage(navImageWidth, navImageHeight,
                image.getType());
        Graphics g = navigationImage.getGraphics();
        g.drawImage(image, 0, 0, navImageWidth, navImageHeight, null);
        g.dispose();
    }

    /**
     * <p>
     * Sets an image for display in the panel.</p>
     *
     * @param image an image to be set in the panel
     */
    public void setImage(BufferedImage image) {
        BufferedImage oldImage = this.image;
        this.image = image;
        //Reset scale so that initializeParameters() is called in paintComponent()
        //for the new image.
        scale = 0.0;
        firePropertyChange(IMAGE_CHANGED_PROPERTY, (Image) oldImage, (Image) image);
        repaint();
    }

    public BufferedImage getImage() {
        return image;
    }

    /**
     * <p>
     * Tests whether an image uses the standard RGB color space.</p>
     */
    public static boolean isStandardRGBImage(BufferedImage bImage) {
        return bImage.getColorModel().getColorSpace().isCS_sRGB();
    }

    //Converts this panel's coordinates into the original image coordinates
    private Coords panelToImageCoords(Point p) {
        return new Coords((p.x - originX) / scale, (p.y - originY) / scale);
    }

    //Converts the original image coordinates into this panel's coordinates
    private Coords imageToPanelCoords(Coords p) {
        return new Coords((p.x * scale) + originX, (p.y * scale) + originY);
    }

    //Converts the navigation image coordinates into the zoomed image coordinates	
    private Point navToZoomedImageCoords(Point p) {
        int x = p.x * getScreenImageWidth() / getScreenNavImageWidth();
        int y = p.y * getScreenImageHeight() / getScreenNavImageHeight();
        return new Point(x, y);
    }

    //The user clicked within the navigation image and this part of the image
    //is displayed in the panel.
    //The clicked point of the image is centered in the panel.
    private void displayImageAt(Point p) {
        Point scrImagePoint = navToZoomedImageCoords(p);
        originX = -(scrImagePoint.x - getWidth() / 2);
        originY = -(scrImagePoint.y - getHeight() / 2);
        repaint();
    }

    //Tests whether a given point in the panel falls within the image boundaries.	
    private boolean isInImage(Point p) {
        Coords coords = panelToImageCoords(p);
        int x = coords.getIntX();
        int y = coords.getIntY();
        return (x >= 0 && x < image.getWidth() && y >= 0 && y < image.getHeight());
    }

    //Tests whether a given point in the panel falls within the navigation image 
    //boundaries.	
    private boolean isInNavigationImage(Point p) {
        return (isNavigationImageEnabled() && p.x < getScreenNavImageWidth()
                && p.y < getScreenNavImageHeight());
    }

    //Used when the image is resized.
    private boolean isImageEdgeInPanel() {
        if (previousPanelSize == null) {
            return false;
        }

        return (originX > 0 && originX < previousPanelSize.width
                || originY > 0 && originY < previousPanelSize.height);
    }

    //Tests whether the image is displayed in its entirety in the panel.
    private boolean isFullImageInPanel() {
        return (originX >= 0 && (originX + getScreenImageWidth()) < getWidth()
                && originY >= 0 && (originY + getScreenImageHeight()) < getHeight());
    }

    /**
     * <p>
     * Indicates whether the high quality rendering feature is enabled.</p>
     *
     * @return true if high quality rendering is enabled, false otherwise.
     */
    public boolean isHighQualityRenderingEnabled() {
        return highQualityRenderingEnabled;
    }

    /**
     * <p>
     * Enables/disables high quality rendering.</p>
     *
     * @param enabled enables/disables high quality rendering
     */
    public void setHighQualityRenderingEnabled(boolean enabled) {
        highQualityRenderingEnabled = enabled;
    }

    //High quality rendering kicks in when when a scaled image is larger 
    //than the original image. In other words, 
    //when image decimation stops and interpolation starts.
    private boolean isHighQualityRendering() {
        return (highQualityRenderingEnabled
                && scale > HIGH_QUALITY_RENDERING_SCALE_THRESHOLD);
    }

    /**
     * <p>
     * Indicates whether navigation image is enabled.<p>
     *
     * @return true when navigation image is enabled, false otherwise.
     */
    public boolean isNavigationImageEnabled() {
        return navigationImageEnabled;
    }

    /**
     * <p>
     * Enables/disables navigation with the navigation image.</p>
     * <p>
     * Navigation image should be disabled when custom, programmatic navigation
     * is implemented.</p>
     *
     * @param enabled true when navigation image is enabled, false otherwise.
     */
    public void setNavigationImageEnabled(boolean enabled) {
        navigationImageEnabled = enabled;
        repaint();
    }

    //Used when the panel is resized	 
    private void scaleOrigin() {
        originX = originX * getWidth() / previousPanelSize.width;
        originY = originY * getHeight() / previousPanelSize.height;
        repaint();
    }

    //Converts the specified zoom level	to scale.
    private double zoomToScale(double zoom) {
        return initialScale * zoom;
    }

    /**
     * <p>
     * Gets the current zoom level.</p>
     *
     * @return the current zoom level
     */
    public double getZoom() {
        return scale / initialScale;
    }

    /**
     * <p>
     * Sets the zoom level used to display the image.</p>
     * <p>
     * This method is used in programmatic zooming. The zooming center is the
     * point of the image closest to the center of the panel. After a new zoom
     * level is set the image is repainted.</p>
     *
     * @param newZoom the zoom level used to display this panel's image.
     */
    public void setZoom(double newZoom) {
        Point zoomingCenter = new Point(getWidth() / 2, getHeight() / 2);
        setZoom(newZoom, zoomingCenter);
    }

    /**
     * <p>
     * Sets the zoom level used to display the image, and the zooming center,
     * around which zooming is done.</p>
     * <p>
     * This method is used in programmatic zooming. After a new zoom level is
     * set the image is repainted.</p>
     *
     * @param newZoom the zoom level used to display this panel's image.
     */
    public void setZoom(double newZoom, Point zoomingCenter) {
        Coords imageP = panelToImageCoords(zoomingCenter);
        if (imageP.x < 0.0) {
            imageP.x = 0.0;
        }
        if (imageP.y < 0.0) {
            imageP.y = 0.0;
        }
        if (imageP.x >= image.getWidth()) {
            imageP.x = image.getWidth() - 1.0;
        }
        if (imageP.y >= image.getHeight()) {
            imageP.y = image.getHeight() - 1.0;
        }

        Coords correctedP = imageToPanelCoords(imageP);
        double oldZoom = getZoom();
        scale = zoomToScale(newZoom);
        Coords panelP = imageToPanelCoords(imageP);

        originX += (correctedP.getIntX() - (int) panelP.x);
        originY += (correctedP.getIntY() - (int) panelP.y);

        firePropertyChange(ZOOM_LEVEL_CHANGED_PROPERTY, new Double(oldZoom),
                new Double(getZoom()));

        repaint();
    }

    /**
     * <p>
     * Gets the current zoom increment.</p>
     *
     * @return the current zoom increment
     */
    public double getZoomIncrement() {
        return zoomIncrement;
    }

    /**
     * <p>
     * Sets a new zoom increment value.</p>
     *
     * @param newZoomIncrement new zoom increment value
     */
    public void setZoomIncrement(double newZoomIncrement) {
        double oldZoomIncrement = zoomIncrement;
        zoomIncrement = newZoomIncrement;
        firePropertyChange(ZOOM_INCREMENT_CHANGED_PROPERTY,
                new Double(oldZoomIncrement), new Double(zoomIncrement));
    }

    //Zooms an image in the panel by repainting it at the new zoom level.
    //The current mouse position is the zooming center.
    private void zoomImage() {
        Coords imageP = panelToImageCoords(mousePosition);
        double oldZoom = getZoom();
        scale *= zoomFactor;
        Coords panelP = imageToPanelCoords(imageP);

        originX += (mousePosition.x - (int) panelP.x);
        originY += (mousePosition.y - (int) panelP.y);

        firePropertyChange(ZOOM_LEVEL_CHANGED_PROPERTY, new Double(oldZoom),
                new Double(getZoom()));

        repaint();
    }

    //Zooms the navigation image
    private void zoomNavigationImage() {
        navScale *= navZoomFactor;
        repaint();
    }

    /**
     * <p>
     * Gets the image origin.</p>
     * <p>
     * Image origin is defined as the upper, left corner of the image in the
     * panel's coordinate system.</p>
     *
     * @return the point of the upper, left corner of the image in the panel's
     * coordinates system.
     */
    public Point getImageOrigin() {
        return new Point(originX, originY);
    }

    /**
     * <p>
     * Sets the image origin.</p>
     * <p>
     * Image origin is defined as the upper, left corner of the image in the
     * panel's coordinate system. After a new origin is set, the image is
     * repainted. This method is used for programmatic image navigation.</p>
     *
     * @param x the x coordinate of the new image origin
     * @param y the y coordinate of the new image origin
     */
    public void setImageOrigin(int x, int y) {
        setImageOrigin(new Point(x, y));
    }

    /**
     * <p>
     * Sets the image origin.</p>
     * <p>
     * Image origin is defined as the upper, left corner of the image in the
     * panel's coordinate system. After a new origin is set, the image is
     * repainted. This method is used for programmatic image navigation.</p>
     *
     * @param newOrigin the value of a new image origin
     */
    public void setImageOrigin(Point newOrigin) {
        originX = newOrigin.x;
        originY = newOrigin.y;
        repaint();
    }

    //Moves te image (by dragging with the mouse) to a new mouse position p.
    private void moveImage(Point p) {
        int xDelta = p.x - mousePosition.x;
        int yDelta = p.y - mousePosition.y;
        originX += xDelta;
        originY += yDelta;
        mousePosition = p;
        repaint();
    }

    //Gets the bounds of the image area currently displayed in the panel (in image
    //coordinates).
    private Rectangle getImageClipBounds() {
        Coords startCoords = panelToImageCoords(new Point(0, 0));
        Coords endCoords = panelToImageCoords(new Point(getWidth() - 1, getHeight() - 1));
        int panelX1 = startCoords.getIntX();
        int panelY1 = startCoords.getIntY();
        int panelX2 = endCoords.getIntX();
        int panelY2 = endCoords.getIntY();
        //No intersection?
        if (panelX1 >= image.getWidth() || panelX2 < 0 || panelY1 >= image.getHeight() || panelY2 < 0) {
            return null;
        }

        int x1 = (panelX1 < 0) ? 0 : panelX1;
        int y1 = (panelY1 < 0) ? 0 : panelY1;
        int x2 = (panelX2 >= image.getWidth()) ? image.getWidth() - 1 : panelX2;
        int y2 = (panelY2 >= image.getHeight()) ? image.getHeight() - 1 : panelY2;
        return new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
    }

    /**
     * Paints the panel and its image at the current zoom level, location, and
     * interpolation method dependent on the image scale.</p>
     *
     * @param g the <code>Graphics</code> context for painting
     */
    protected void paintComponent(Graphics g) {
        super.paintComponent(g); // Paints the background

        if (image == null) {
            return;
        }

        if (scale == 0.0) {
            initializeParams();
        }

        if (isHighQualityRendering()) {
            Rectangle rect = getImageClipBounds();
            if (rect == null || rect.width == 0 || rect.height == 0) { // no part of image is displayed in the panel
                return;
            }

            BufferedImage subimage = image.getSubimage(rect.x, rect.y, rect.width,
                    rect.height);
            Graphics2D g2 = (Graphics2D) g;
            /*
             // modified by Bernie to use getScaledInstance, which returns a better image
             g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, INTERPOLATION_TYPE);
             g2.drawImage(subimage, Math.max(0, originX), Math.max(0, originY),
             Math.min((int) (subimage.getWidth() * scale), getWidth()),
             Math.min((int) (subimage.getHeight() * scale), getHeight()), null);
             */
            BufferedImage scaledImage = ImageUtils.getScaledInstance(subimage,
                    Math.min((int) (subimage.getWidth() * scale), getWidth()),
                    Math.min((int) (subimage.getHeight() * scale), getHeight()),
                    INTERPOLATION_TYPE, true);
            g2.drawImage(scaledImage, Math.max(0, originX), Math.max(0, originY), null);
        } else {
            g.drawImage(image, originX, originY, getScreenImageWidth(),
                    getScreenImageHeight(), null);
        }

        //Draw navigation image
        if (isNavigationImageEnabled()) {
            g.drawImage(navigationImage, 0, 0, getScreenNavImageWidth(),
                    getScreenNavImageHeight(), null);
            drawZoomAreaOutline(g);
        }
    }

    //Paints a white outline over the navigation image indicating 
    //the area of the image currently displayed in the panel.
    private void drawZoomAreaOutline(Graphics g) {
        if (isFullImageInPanel()) {
            return;
        }

        int x = -originX * getScreenNavImageWidth() / getScreenImageWidth();
        int y = -originY * getScreenNavImageHeight() / getScreenImageHeight();
        int width = getWidth() * getScreenNavImageWidth() / getScreenImageWidth();
        int height = getHeight() * getScreenNavImageHeight() / getScreenImageHeight();
        g.setColor(Color.white);
        g.drawRect(x, y, width, height);
    }

    private int getScreenImageWidth() {
        return (int) (scale * image.getWidth());
    }

    private int getScreenImageHeight() {
        return (int) (scale * image.getHeight());
    }

    private int getScreenNavImageWidth() {
        return (int) (navScale * navImageWidth);
    }

    private int getScreenNavImageHeight() {
        return (int) (navScale * navImageHeight);
    }

    private static String[] getImageFormatExtensions() {
        String[] names = ImageIO.getReaderFormatNames();
        for (int i = 0; i < names.length; i++) {
            names[i] = names[i].toLowerCase();
        }
        Arrays.sort(names);
        return names;
    }

}
