StreetsideImageDisplay.java
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.streetside.gui;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BasicStroke;
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.Shape;
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.util.ArrayList;
import java.util.Collection;
import javax.swing.JComponent;
import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
import org.openstreetmap.josm.plugins.streetside.actions.StreetsideDownloadAction;
import org.openstreetmap.josm.plugins.streetside.model.ImageDetection;
import org.openstreetmap.josm.plugins.streetside.model.MapObject;
import org.openstreetmap.josm.plugins.streetside.utils.StreetsideColorScheme;
import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
/**
* This object is a responsible JComponent which lets you zoom and drag. It is
* included in a {@link StreetsideMainDialog} object.
*
* @author nokutu
* @see StreetsideImageDisplay
* @see StreetsideMainDialog
*/
public class StreetsideImageDisplay extends JComponent {
private static final long serialVersionUID = -3188274185432686201L;
private final Collection<ImageDetection> detections = new ArrayList<>();
/** The image currently displayed */
private volatile BufferedImage image;
/**
* The rectangle (in image coordinates) of the image that is visible. This
* rectangle is calculated each time the zoom is modified
*/
private volatile Rectangle visibleRect;
/**
* When a selection is done, the rectangle of the selection (in image
* coordinates)
*/
private Rectangle selectedRect;
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) {
Image image;
Rectangle visibleRect;
synchronized (StreetsideImageDisplay.this) {
image = getImage();
visibleRect = StreetsideImageDisplay.this.visibleRect;
}
mouseIsDragging = false;
selectedRect = null;
if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) {
// 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());
}
// Set 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);
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 (StreetsideImageDisplay.this) {
StreetsideImageDisplay.this.visibleRect = visibleRect;
}
StreetsideImageDisplay.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.
Image image;
Rectangle visibleRect;
synchronized (StreetsideImageDisplay.this) {
image = getImage();
visibleRect = StreetsideImageDisplay.this.visibleRect;
}
if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) {
if (e.getButton() == StreetsideProperties.PICTURE_OPTION_BUTTON.get()) {
if (!StreetsideImageDisplay.this.visibleRect.equals(new Rectangle(0, 0, image.getWidth(null), image.getHeight(null)))) {
// Zooms to 1:1
StreetsideImageDisplay.this.visibleRect = new Rectangle(0, 0,
image.getWidth(null), image.getHeight(null));
} else {
// Zooms to best fit.
StreetsideImageDisplay.this.visibleRect = new Rectangle(
0,
(image.getHeight(null) - (image.getWidth(null) * getHeight()) / getWidth()) / 2,
image.getWidth(null),
(image.getWidth(null) * getHeight()) / getWidth()
);
}
StreetsideImageDisplay.this.repaint();
return;
} else if (e.getButton() != StreetsideProperties.PICTURE_DRAG_BUTTON.get()) {
return;
}
// Calculate the translation to set the clicked point the center of
// the view.
Point click = comp2imgCoord(visibleRect, e.getX(), e.getY());
Point center = getCenterImgCoord(visibleRect);
visibleRect.x += click.x - center.x;
visibleRect.y += click.y - center.y;
checkVisibleRectPos(image, visibleRect);
synchronized (StreetsideImageDisplay.this) {
StreetsideImageDisplay.this.visibleRect = visibleRect;
}
StreetsideImageDisplay.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 (getImage() == null) {
mouseIsDragging = false;
selectedRect = null;
return;
}
Image image;
Rectangle visibleRect;
synchronized (StreetsideImageDisplay.this) {
image = StreetsideImageDisplay.this.image;
visibleRect = StreetsideImageDisplay.this.visibleRect;
}
if (image == null)
return;
if (e.getButton() == StreetsideProperties.PICTURE_DRAG_BUTTON.get()) {
mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
mouseIsDragging = true;
selectedRect = null;
} else if (e.getButton() == StreetsideProperties.PICTURE_ZOOM_BUTTON.get()) {
mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
checkPointInVisibleRect(mousePointInImg, visibleRect);
mouseIsDragging = false;
selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
StreetsideImageDisplay.this.repaint();
} else {
mouseIsDragging = false;
selectedRect = null;
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (!mouseIsDragging && selectedRect == null)
return;
Image image;
Rectangle visibleRect;
synchronized (StreetsideImageDisplay.this) {
image = getImage();
visibleRect = StreetsideImageDisplay.this.visibleRect;
}
if (image == null) {
mouseIsDragging = false;
selectedRect = null;
return;
}
if (mouseIsDragging) {
Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
visibleRect.x += mousePointInImg.x - p.x;
visibleRect.y += mousePointInImg.y - p.y;
checkVisibleRectPos(image, visibleRect);
synchronized (StreetsideImageDisplay.this) {
StreetsideImageDisplay.this.visibleRect = visibleRect;
}
StreetsideImageDisplay.this.repaint();
} else if (selectedRect != null) {
Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
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);
selectedRect = rect;
StreetsideImageDisplay.this.repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (!mouseIsDragging && selectedRect == null)
return;
Image image;
synchronized (StreetsideImageDisplay.this) {
image = getImage();
}
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 (StreetsideImageDisplay.this) {
visibleRect = selectedRect;
}
selectedRect = null;
StreetsideImageDisplay.this.repaint();
}
}
@Override
public void mouseEntered(MouseEvent e) {
// Do nothing, method is enforced by MouseListener
}
@Override
public void mouseExited(MouseEvent e) {
// Do nothing, method is enforced by MouseListener
}
@Override
public void mouseMoved(MouseEvent e) {
// Do nothing, method is enforced by MouseListener
}
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;
}
}
}
/**
* Main constructor.
*/
public StreetsideImageDisplay() {
ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
addMouseListener(mouseListener);
addMouseWheelListener(mouseListener);
addMouseMotionListener(mouseListener);
StreetsideProperties.SHOW_DETECTED_SIGNS.addListener(valChanged -> repaint());
}
/**
* Sets a new picture to be displayed.
*
* @param image The picture to be displayed.
* @param detections image detections
*/
public void setImage(BufferedImage image, Collection<ImageDetection> detections) {
synchronized (this) {
this.image = image;
this.detections.clear();
if (detections != null) {
this.detections.addAll(detections);
}
selectedRect = null;
if (image != null)
visibleRect = new Rectangle(0, 0, image.getWidth(null),
image.getHeight(null));
}
repaint();
}
/**
* Returns the picture that is being displayed
*
* @return The picture that is being displayed.
*/
public BufferedImage getImage() {
return image;
}
/**
* Paints the visible part of the picture.
*/
@Override
public void paintComponent(Graphics g) {
Image image;
Rectangle visibleRect;
synchronized (this) {
image = this.image;
visibleRect = this.visibleRect;
}
if (image == null) {
g.setColor(Color.black);
String noImageStr = StreetsideLayer.hasInstance() ? tr("No image selected") : tr("Press \"{0}\" to download images", StreetsideDownloadAction.SHORTCUT.getKeyText());
Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(
noImageStr, g);
Dimension size = getSize();
g.drawString(noImageStr,
(int) ((size.width - noImageSize.getWidth()) / 2),
(int) ((size.height - noImageSize.getHeight()) / 2));
} else {
Rectangle target = calculateDrawImageRectangle(visibleRect);
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);
Point bottomRight = img2compCoord(visibleRect, selectedRect.x
+ selectedRect.width, selectedRect.y + selectedRect.height);
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 (StreetsideProperties.SHOW_DETECTED_SIGNS.get()) {
Point upperLeft = img2compCoord(visibleRect, 0, 0);
Point lowerRight = img2compCoord(visibleRect, getImage().getWidth(), getImage().getHeight());
// Transformation, which can convert you a Shape relative to the unit square to a Shape relative to the Component
AffineTransform unit2compTransform = AffineTransform.getTranslateInstance(upperLeft.getX(), upperLeft.getY());
unit2compTransform.concatenate(AffineTransform.getScaleInstance(lowerRight.getX() - upperLeft.getX(), lowerRight.getY() - upperLeft.getY()));
final Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(2));
for (ImageDetection d : detections) {
final Shape shape = d.getShape().createTransformedShape(unit2compTransform);
g2d.setColor(d.isTrafficSign() ? StreetsideColorScheme.IMAGEDETECTION_TRAFFICSIGN : StreetsideColorScheme.IMAGEDETECTION_UNKNOWN);
g2d.draw(shape);
if (d.isTrafficSign()) {
g2d.drawImage(
MapObject.getIcon(d.getValue()).getImage(),
shape.getBounds().x, shape.getBounds().y,
shape.getBounds().width, shape.getBounds().height,
null
);
}
}
}
}
}
private Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) {
Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width)
/ visibleRect.width, drawRect.y
+ ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
}
private Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) {
Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
return new Point(
visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height
);
}
private static Point getCenterImgCoord(Rectangle visibleRect) {
return new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2);
}
private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) {
return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, getSize().width, getSize().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
*/
private 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);
}
/**
* Zooms to 1:1 and, if it is already in 1:1, to best fit.
*/
public void zoomBestFitOrOne() {
Image image;
Rectangle visibleRect;
synchronized (this) {
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) {
this.visibleRect = visibleRect;
}
repaint();
}
private 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;
}
}
private 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);
}
}
}