// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.layer.geoimage;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.swing.JComponent;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.tools.ExifReader;
public class ImageDisplay extends JComponent {
/** The file that is currently displayed */
private File file;
/** The image currently displayed */
private transient Image image;
/** The image currently displayed */
private boolean errorLoading;
/** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
* each time the zoom is modified */
private Rectangle visibleRect;
/** When a selection is done, the rectangle of the selection (in image coordinates) */
private Rectangle selectedRect;
/** The tracker to load the images */
private final MediaTracker tracker = new MediaTracker(this);
private String osdText;
private static final int DRAG_BUTTON = Main.pref.getBoolean("geoimage.agpifo-style-drag-and-zoom", false) ? 1 : 3;
private static final int ZOOM_BUTTON = DRAG_BUTTON == 1 ? 3 : 1;
/** The thread that reads the images. */
private class LoadImageRunnable implements Runnable {
private final File file;
private final int orientation;
LoadImageRunnable(File file, Integer orientation) {
this.file = file;
this.orientation = orientation == null ? -1 : orientation;
}
@Override
public void run() {
Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
tracker.addImage(img, 1);
// Wait for the end of loading
while (!tracker.checkID(1, true)) {
if (this.file != ImageDisplay.this.file) {
// The file has changed
tracker.removeImage(img);
return;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Main.warn("InterruptedException in "+getClass().getSimpleName()+" while loading image "+file.getPath());
Thread.currentThread().interrupt();
}
}
boolean error = tracker.isErrorID(1);
if (img.getWidth(null) < 0 || img.getHeight(null) < 0) {
error = true;
}
synchronized (ImageDisplay.this) {
if (this.file != ImageDisplay.this.file) {
// The file has changed
tracker.removeImage(img);
return;
}
if (!error) {
ImageDisplay.this.image = img;
visibleRect = new Rectangle(0, 0, img.getWidth(null), img.getHeight(null));
final int w = (int) visibleRect.getWidth();
final int h = (int) visibleRect.getHeight();
if (ExifReader.orientationNeedsCorrection(orientation)) {
final int hh, ww;
if (ExifReader.orientationSwitchesDimensions(orientation)) {
ww = h;
hh = w;
} else {
ww = w;
hh = h;
}
final BufferedImage rot = new BufferedImage(ww, hh, BufferedImage.TYPE_INT_RGB);
final AffineTransform xform = ExifReader.getRestoreOrientationTransform(orientation, w, h);
final Graphics2D g = rot.createGraphics();
g.drawImage(image, xform, null);
g.dispose();
visibleRect.setSize(ww, hh);
image.flush();
ImageDisplay.this.image = rot;
}
}
selectedRect = null;
errorLoading = error;
}
tracker.removeImage(img);
ImageDisplay.this.repaint();
}
}
private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
private boolean mouseIsDragging;
private long lastTimeForMousePoint;
private Point mousePointInImg;
/** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
* at the same place */
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
File file;
Image image;
Rectangle visibleRect;
synchronized (ImageDisplay.this) {
file = ImageDisplay.this.file;
image = ImageDisplay.this.image;
visibleRect = ImageDisplay.this.visibleRect;
}
mouseIsDragging = false;
selectedRect = null;
if (image == null)
return;
// Calculate the mouse cursor position in image coordinates, so that we can center the zoom
// on that mouse position.
// To avoid issues when the user tries to zoom in on the image borders, this point is not calculated
// again if there was less than 1.5seconds since the last event.
if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) {
lastTimeForMousePoint = e.getWhen();
mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
}
// Applicate the zoom to the visible rectangle in image coordinates
if (e.getWheelRotation() > 0) {
visibleRect.width = visibleRect.width * 3 / 2;
visibleRect.height = visibleRect.height * 3 / 2;
} else {
visibleRect.width = visibleRect.width * 2 / 3;
visibleRect.height = visibleRect.height * 2 / 3;
}
// Check that the zoom doesn't exceed 2:1
if (visibleRect.width < getSize().width / 2) {
visibleRect.width = getSize().width / 2;
}
if (visibleRect.height < getSize().height / 2) {
visibleRect.height = getSize().height / 2;
}
// Set the same ratio for the visible rectangle and the display area
int hFact = visibleRect.height * getSize().width;
int wFact = visibleRect.width * getSize().height;
if (hFact > wFact) {
visibleRect.width = hFact / getSize().height;
} else {
visibleRect.height = wFact / getSize().width;
}
// The size of the visible rectangle is limited by the image size.
checkVisibleRectSize(image, visibleRect);
// Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
Rectangle drawRect = calculateDrawImageRectangle(visibleRect, getSize());
visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
// The position is also limited by the image size
checkVisibleRectPos(image, visibleRect);
synchronized (ImageDisplay.this) {
if (ImageDisplay.this.file == file) {
ImageDisplay.this.visibleRect = visibleRect;
}
}
ImageDisplay.this.repaint();
}
/** Center the display on the point that has been clicked */
@Override
public void mouseClicked(MouseEvent e) {
// Move the center to the clicked point.
File file;
Image image;
Rectangle visibleRect;
synchronized (ImageDisplay.this) {
file = ImageDisplay.this.file;
image = ImageDisplay.this.image;
visibleRect = ImageDisplay.this.visibleRect;
}
if (image == null)
return;
if (e.getButton() != DRAG_BUTTON)
return;
// Calculate the translation to set the clicked point the center of the view.
Point click = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
Point center = getCenterImgCoord(visibleRect);
visibleRect.x += click.x - center.x;
visibleRect.y += click.y - center.y;
checkVisibleRectPos(image, visibleRect);
synchronized (ImageDisplay.this) {
if (ImageDisplay.this.file == file) {
ImageDisplay.this.visibleRect = visibleRect;
}
}
ImageDisplay.this.repaint();
}
/** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of
* a picture part) */
@Override
public void mousePressed(MouseEvent e) {
if (image == null) {
mouseIsDragging = false;
selectedRect = null;
return;
}
Image image;
Rectangle visibleRect;
synchronized (ImageDisplay.this) {
image = ImageDisplay.this.image;
visibleRect = ImageDisplay.this.visibleRect;
}
if (image == null)
return;
if (e.getButton() == DRAG_BUTTON) {
mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
mouseIsDragging = true;
selectedRect = null;
} else if (e.getButton() == ZOOM_BUTTON) {
mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
checkPointInVisibleRect(mousePointInImg, visibleRect);
mouseIsDragging = false;
selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
ImageDisplay.this.repaint();
} else {
mouseIsDragging = false;
selectedRect = null;
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (!mouseIsDragging && selectedRect == null)
return;
File file;
Image image;
Rectangle visibleRect;
synchronized (ImageDisplay.this) {
file = ImageDisplay.this.file;
image = ImageDisplay.this.image;
visibleRect = ImageDisplay.this.visibleRect;
}
if (image == null) {
mouseIsDragging = false;
selectedRect = null;
return;
}
if (mouseIsDragging) {
Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
visibleRect.x += mousePointInImg.x - p.x;
visibleRect.y += mousePointInImg.y - p.y;
checkVisibleRectPos(image, visibleRect);
synchronized (ImageDisplay.this) {
if (ImageDisplay.this.file == file) {
ImageDisplay.this.visibleRect = visibleRect;
}
}
ImageDisplay.this.repaint();
} else if (selectedRect != null) {
Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
checkPointInVisibleRect(p, visibleRect);
Rectangle rect = new Rectangle(
p.x < mousePointInImg.x ? p.x : mousePointInImg.x,
p.y < mousePointInImg.y ? p.y : mousePointInImg.y,
p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y);
checkVisibleRectSize(image, rect);
checkVisibleRectPos(image, rect);
ImageDisplay.this.selectedRect = rect;
ImageDisplay.this.repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (!mouseIsDragging && selectedRect == null)
return;
File file;
Image image;
synchronized (ImageDisplay.this) {
file = ImageDisplay.this.file;
image = ImageDisplay.this.image;
}
if (image == null) {
mouseIsDragging = false;
selectedRect = null;
return;
}
if (mouseIsDragging) {
mouseIsDragging = false;
} else if (selectedRect != null) {
int oldWidth = selectedRect.width;
int oldHeight = selectedRect.height;
// Check that the zoom doesn't exceed 2:1
if (selectedRect.width < getSize().width / 2) {
selectedRect.width = getSize().width / 2;
}
if (selectedRect.height < getSize().height / 2) {
selectedRect.height = getSize().height / 2;
}
// Set the same ratio for the visible rectangle and the display area
int hFact = selectedRect.height * getSize().width;
int wFact = selectedRect.width * getSize().height;
if (hFact > wFact) {
selectedRect.width = hFact / getSize().height;
} else {
selectedRect.height = wFact / getSize().width;
}
// Keep the center of the selection
if (selectedRect.width != oldWidth) {
selectedRect.x -= (selectedRect.width - oldWidth) / 2;
}
if (selectedRect.height != oldHeight) {
selectedRect.y -= (selectedRect.height - oldHeight) / 2;
}
checkVisibleRectSize(image, selectedRect);
checkVisibleRectPos(image, selectedRect);
synchronized (ImageDisplay.this) {
if (file == ImageDisplay.this.file) {
ImageDisplay.this.visibleRect = selectedRect;
}
}
selectedRect = null;
ImageDisplay.this.repaint();
}
}
@Override
public void mouseEntered(MouseEvent e) {
// Do nothing
}
@Override
public void mouseExited(MouseEvent e) {
// Do nothing
}
@Override
public void mouseMoved(MouseEvent e) {
// Do nothing
}
private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
if (p.x < visibleRect.x) {
p.x = visibleRect.x;
}
if (p.x > visibleRect.x + visibleRect.width) {
p.x = visibleRect.x + visibleRect.width;
}
if (p.y < visibleRect.y) {
p.y = visibleRect.y;
}
if (p.y > visibleRect.y + visibleRect.height) {
p.y = visibleRect.y + visibleRect.height;
}
}
}
/**
* Constructs a new {@code ImageDisplay}.
*/
public ImageDisplay() {
ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
addMouseListener(mouseListener);
addMouseWheelListener(mouseListener);
addMouseMotionListener(mouseListener);
}
public void setImage(File file, Integer orientation) {
synchronized (this) {
this.file = file;
image = null;
selectedRect = null;
errorLoading = false;
}
repaint();
if (file != null) {
new Thread(new LoadImageRunnable(file, orientation), LoadImageRunnable.class.getName()).start();
}
}
public void setOsdText(String text) {
this.osdText = text;
repaint();
}
@Override
public void paintComponent(Graphics g) {
Image image;
File file;
Rectangle visibleRect;
boolean errorLoading;
synchronized (this) {
image = this.image;
file = this.file;
visibleRect = this.visibleRect;
errorLoading = this.errorLoading;
}
Dimension size = getSize();
if (file == null) {
g.setColor(Color.black);
String noImageStr = tr("No image");
Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
g.drawString(noImageStr,
(int) ((size.width - noImageSize.getWidth()) / 2),
(int) ((size.height - noImageSize.getHeight()) / 2));
} else if (image == null) {
g.setColor(Color.black);
String loadingStr;
if (!errorLoading) {
loadingStr = tr("Loading {0}", file.getName());
} else {
loadingStr = tr("Error on file {0}", file.getName());
}
Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
g.drawString(loadingStr,
(int) ((size.width - noImageSize.getWidth()) / 2),
(int) ((size.height - noImageSize.getHeight()) / 2));
} else {
Rectangle target = calculateDrawImageRectangle(visibleRect, size);
g.drawImage(image,
target.x, target.y, target.x + target.width, target.y + target.height,
visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height,
null);
if (selectedRect != null) {
Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size);
Point bottomRight = img2compCoord(visibleRect,
selectedRect.x + selectedRect.width,
selectedRect.y + selectedRect.height, size);
g.setColor(new Color(128, 128, 128, 180));
g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
g.setColor(Color.black);
g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
}
if (errorLoading) {
String loadingStr = tr("Error on file {0}", file.getName());
Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
g.drawString(loadingStr,
(int) ((size.width - noImageSize.getWidth()) / 2),
(int) ((size.height - noImageSize.getHeight()) / 2));
}
if (osdText != null) {
FontMetrics metrics = g.getFontMetrics(g.getFont());
int ascent = metrics.getAscent();
Color bkground = new Color(255, 255, 255, 128);
int lastPos = 0;
int pos = osdText.indexOf('\n');
int x = 3;
int y = 3;
String line;
while (pos > 0) {
line = osdText.substring(lastPos, pos);
Rectangle2D lineSize = metrics.getStringBounds(line, g);
g.setColor(bkground);
g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
g.setColor(Color.black);
g.drawString(line, x, y + ascent);
y += (int) lineSize.getHeight();
lastPos = pos + 1;
pos = osdText.indexOf('\n', lastPos);
}
line = osdText.substring(lastPos);
Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g);
g.setColor(bkground);
g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
g.setColor(Color.black);
g.drawString(line, x, y + ascent);
}
}
}
static Point img2compCoord(Rectangle visibleRect, int xImg, int yImg, Dimension compSize) {
Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
}
static Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp, Dimension compSize) {
Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height);
}
static Point getCenterImgCoord(Rectangle visibleRect) {
return new Point(visibleRect.x + visibleRect.width / 2,
visibleRect.y + visibleRect.height / 2);
}
static Rectangle calculateDrawImageRectangle(Rectangle visibleRect, Dimension compSize) {
return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height));
}
/**
* calculateDrawImageRectangle
*
* @param imgRect the part of the image that should be drawn (in image coordinates)
* @param compRect the part of the component where the image should be drawn (in component coordinates)
* @return the part of compRect with the same width/height ratio as the image
*/
static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) {
int x = 0;
int y = 0;
int w = compRect.width;
int h = compRect.height;
int wFact = w * imgRect.height;
int hFact = h * imgRect.width;
if (wFact != hFact) {
if (wFact > hFact) {
w = hFact / imgRect.height;
x = (compRect.width - w) / 2;
} else {
h = wFact / imgRect.width;
y = (compRect.height - h) / 2;
}
}
return new Rectangle(x + compRect.x, y + compRect.y, w, h);
}
public void zoomBestFitOrOne() {
File file;
Image image;
Rectangle visibleRect;
synchronized (this) {
file = this.file;
image = this.image;
visibleRect = this.visibleRect;
}
if (image == null)
return;
if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
// The display is not at best fit. => Zoom to best fit
visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
} else {
// The display is at best fit => zoom to 1:1
Point center = getCenterImgCoord(visibleRect);
visibleRect = new Rectangle(center.x - getWidth() / 2, center.y - getHeight() / 2,
getWidth(), getHeight());
checkVisibleRectPos(image, visibleRect);
}
synchronized (this) {
if (file == this.file) {
this.visibleRect = visibleRect;
}
}
repaint();
}
static void checkVisibleRectPos(Image image, Rectangle visibleRect) {
if (visibleRect.x < 0) {
visibleRect.x = 0;
}
if (visibleRect.y < 0) {
visibleRect.y = 0;
}
if (visibleRect.x + visibleRect.width > image.getWidth(null)) {
visibleRect.x = image.getWidth(null) - visibleRect.width;
}
if (visibleRect.y + visibleRect.height > image.getHeight(null)) {
visibleRect.y = image.getHeight(null) - visibleRect.height;
}
}
static void checkVisibleRectSize(Image image, Rectangle visibleRect) {
if (visibleRect.width > image.getWidth(null)) {
visibleRect.width = image.getWidth(null);
}
if (visibleRect.height > image.getHeight(null)) {
visibleRect.height = image.getHeight(null);
}
}
}