001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins.streetside.gui; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.BasicStroke; 007import java.awt.Color; 008import java.awt.Dimension; 009import java.awt.Graphics; 010import java.awt.Graphics2D; 011import java.awt.Image; 012import java.awt.Point; 013import java.awt.Rectangle; 014import java.awt.Shape; 015import java.awt.event.MouseEvent; 016import java.awt.event.MouseListener; 017import java.awt.event.MouseMotionListener; 018import java.awt.event.MouseWheelEvent; 019import java.awt.event.MouseWheelListener; 020import java.awt.geom.AffineTransform; 021import java.awt.geom.Rectangle2D; 022import java.awt.image.BufferedImage; 023import java.util.ArrayList; 024import java.util.Collection; 025 026import javax.swing.JComponent; 027 028import org.openstreetmap.josm.plugins.streetside.StreetsideLayer; 029import org.openstreetmap.josm.plugins.streetside.actions.StreetsideDownloadAction; 030import org.openstreetmap.josm.plugins.streetside.model.ImageDetection; 031import org.openstreetmap.josm.plugins.streetside.model.MapObject; 032import org.openstreetmap.josm.plugins.streetside.utils.StreetsideColorScheme; 033import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties; 034 035 036/** 037 * This object is a responsible JComponent which lets you zoom and drag. It is 038 * included in a {@link StreetsideMainDialog} object. 039 * 040 * @author nokutu 041 * @see StreetsideImageDisplay 042 * @see StreetsideMainDialog 043 */ 044public class StreetsideImageDisplay extends JComponent { 045 046 private static final long serialVersionUID = -3188274185432686201L; 047 048 private final Collection<ImageDetection> detections = new ArrayList<>(); 049 050 /** The image currently displayed */ 051 private volatile BufferedImage image; 052 053 /** 054 * The rectangle (in image coordinates) of the image that is visible. This 055 * rectangle is calculated each time the zoom is modified 056 */ 057 private volatile Rectangle visibleRect; 058 059 /** 060 * When a selection is done, the rectangle of the selection (in image 061 * coordinates) 062 */ 063 private Rectangle selectedRect; 064 065 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener { 066 private boolean mouseIsDragging; 067 private long lastTimeForMousePoint; 068 private Point mousePointInImg; 069 070 /** 071 * Zoom in and out, trying to preserve the point of the image that was under 072 * the mouse cursor at the same place 073 */ 074 @Override 075 public void mouseWheelMoved(MouseWheelEvent e) { 076 Image image; 077 Rectangle visibleRect; 078 synchronized (StreetsideImageDisplay.this) { 079 image = getImage(); 080 visibleRect = StreetsideImageDisplay.this.visibleRect; 081 } 082 mouseIsDragging = false; 083 selectedRect = null; 084 if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) { 085 // Calculate the mouse cursor position in image coordinates, so that 086 // we can center the zoom 087 // on that mouse position. 088 // To avoid issues when the user tries to zoom in on the image 089 // borders, this point is not calculated 090 // again if there was less than 1.5seconds since the last event. 091 if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) { 092 lastTimeForMousePoint = e.getWhen(); 093 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY()); 094 } 095 // Set the zoom to the visible rectangle in image coordinates 096 if (e.getWheelRotation() > 0) { 097 visibleRect.width = visibleRect.width * 3 / 2; 098 visibleRect.height = visibleRect.height * 3 / 2; 099 } else { 100 visibleRect.width = visibleRect.width * 2 / 3; 101 visibleRect.height = visibleRect.height * 2 / 3; 102 } 103 // Check that the zoom doesn't exceed 2:1 104 if (visibleRect.width < getSize().width / 2) { 105 visibleRect.width = getSize().width / 2; 106 } 107 if (visibleRect.height < getSize().height / 2) { 108 visibleRect.height = getSize().height / 2; 109 } 110 // Set the same ratio for the visible rectangle and the display area 111 int hFact = visibleRect.height * getSize().width; 112 int wFact = visibleRect.width * getSize().height; 113 if (hFact > wFact) { 114 visibleRect.width = hFact / getSize().height; 115 } else { 116 visibleRect.height = wFact / getSize().width; 117 } 118 // The size of the visible rectangle is limited by the image size. 119 checkVisibleRectSize(image, visibleRect); 120 // Set the position of the visible rectangle, so that the mouse 121 // cursor doesn't move on the image. 122 Rectangle drawRect = calculateDrawImageRectangle(visibleRect); 123 visibleRect.x = mousePointInImg.x 124 + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width; 125 visibleRect.y = mousePointInImg.y 126 + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height; 127 // The position is also limited by the image size 128 checkVisibleRectPos(image, visibleRect); 129 synchronized (StreetsideImageDisplay.this) { 130 StreetsideImageDisplay.this.visibleRect = visibleRect; 131 } 132 StreetsideImageDisplay.this.repaint(); 133 } 134 } 135 136 /** Center the display on the point that has been clicked */ 137 @Override 138 public void mouseClicked(MouseEvent e) { 139 // Move the center to the clicked point. 140 Image image; 141 Rectangle visibleRect; 142 synchronized (StreetsideImageDisplay.this) { 143 image = getImage(); 144 visibleRect = StreetsideImageDisplay.this.visibleRect; 145 } 146 if (image != null && Math.min(getSize().getWidth(), getSize().getHeight()) > 0) { 147 if (e.getButton() == StreetsideProperties.PICTURE_OPTION_BUTTON.get()) { 148 if (!StreetsideImageDisplay.this.visibleRect.equals(new Rectangle(0, 0, image.getWidth(null), image.getHeight(null)))) { 149 // Zooms to 1:1 150 StreetsideImageDisplay.this.visibleRect = new Rectangle(0, 0, 151 image.getWidth(null), image.getHeight(null)); 152 } else { 153 // Zooms to best fit. 154 StreetsideImageDisplay.this.visibleRect = new Rectangle( 155 0, 156 (image.getHeight(null) - (image.getWidth(null) * getHeight()) / getWidth()) / 2, 157 image.getWidth(null), 158 (image.getWidth(null) * getHeight()) / getWidth() 159 ); 160 } 161 StreetsideImageDisplay.this.repaint(); 162 return; 163 } else if (e.getButton() != StreetsideProperties.PICTURE_DRAG_BUTTON.get()) { 164 return; 165 } 166 // Calculate the translation to set the clicked point the center of 167 // the view. 168 Point click = comp2imgCoord(visibleRect, e.getX(), e.getY()); 169 Point center = getCenterImgCoord(visibleRect); 170 visibleRect.x += click.x - center.x; 171 visibleRect.y += click.y - center.y; 172 checkVisibleRectPos(image, visibleRect); 173 synchronized (StreetsideImageDisplay.this) { 174 StreetsideImageDisplay.this.visibleRect = visibleRect; 175 } 176 StreetsideImageDisplay.this.repaint(); 177 } 178 } 179 180 /** 181 * Initialize the dragging, either with button 1 (simple dragging) or button 182 * 3 (selection of a picture part) 183 */ 184 @Override 185 public void mousePressed(MouseEvent e) { 186 if (getImage() == null) { 187 mouseIsDragging = false; 188 selectedRect = null; 189 return; 190 } 191 Image image; 192 Rectangle visibleRect; 193 synchronized (StreetsideImageDisplay.this) { 194 image = StreetsideImageDisplay.this.image; 195 visibleRect = StreetsideImageDisplay.this.visibleRect; 196 } 197 if (image == null) 198 return; 199 if (e.getButton() == StreetsideProperties.PICTURE_DRAG_BUTTON.get()) { 200 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY()); 201 mouseIsDragging = true; 202 selectedRect = null; 203 } else if (e.getButton() == StreetsideProperties.PICTURE_ZOOM_BUTTON.get()) { 204 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY()); 205 checkPointInVisibleRect(mousePointInImg, visibleRect); 206 mouseIsDragging = false; 207 selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0); 208 StreetsideImageDisplay.this.repaint(); 209 } else { 210 mouseIsDragging = false; 211 selectedRect = null; 212 } 213 } 214 215 @Override 216 public void mouseDragged(MouseEvent e) { 217 if (!mouseIsDragging && selectedRect == null) 218 return; 219 Image image; 220 Rectangle visibleRect; 221 synchronized (StreetsideImageDisplay.this) { 222 image = getImage(); 223 visibleRect = StreetsideImageDisplay.this.visibleRect; 224 } 225 if (image == null) { 226 mouseIsDragging = false; 227 selectedRect = null; 228 return; 229 } 230 if (mouseIsDragging) { 231 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY()); 232 visibleRect.x += mousePointInImg.x - p.x; 233 visibleRect.y += mousePointInImg.y - p.y; 234 checkVisibleRectPos(image, visibleRect); 235 synchronized (StreetsideImageDisplay.this) { 236 StreetsideImageDisplay.this.visibleRect = visibleRect; 237 } 238 StreetsideImageDisplay.this.repaint(); 239 } else if (selectedRect != null) { 240 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY()); 241 checkPointInVisibleRect(p, visibleRect); 242 Rectangle rect = new Rectangle(p.x < mousePointInImg.x ? p.x 243 : mousePointInImg.x, p.y < mousePointInImg.y ? p.y 244 : mousePointInImg.y, p.x < mousePointInImg.x ? mousePointInImg.x 245 - p.x : p.x - mousePointInImg.x, 246 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y 247 - mousePointInImg.y); 248 checkVisibleRectSize(image, rect); 249 checkVisibleRectPos(image, rect); 250 selectedRect = rect; 251 StreetsideImageDisplay.this.repaint(); 252 } 253 } 254 255 @Override 256 public void mouseReleased(MouseEvent e) { 257 if (!mouseIsDragging && selectedRect == null) 258 return; 259 Image image; 260 synchronized (StreetsideImageDisplay.this) { 261 image = getImage(); 262 } 263 if (image == null) { 264 mouseIsDragging = false; 265 selectedRect = null; 266 return; 267 } 268 if (mouseIsDragging) { 269 mouseIsDragging = false; 270 } else if (selectedRect != null) { 271 int oldWidth = selectedRect.width; 272 int oldHeight = selectedRect.height; 273 // Check that the zoom doesn't exceed 2:1 274 if (selectedRect.width < getSize().width / 2) { 275 selectedRect.width = getSize().width / 2; 276 } 277 if (selectedRect.height < getSize().height / 2) { 278 selectedRect.height = getSize().height / 2; 279 } 280 // Set the same ratio for the visible rectangle and the display 281 // area 282 int hFact = selectedRect.height * getSize().width; 283 int wFact = selectedRect.width * getSize().height; 284 if (hFact > wFact) { 285 selectedRect.width = hFact / getSize().height; 286 } else { 287 selectedRect.height = wFact / getSize().width; 288 } 289 // Keep the center of the selection 290 if (selectedRect.width != oldWidth) { 291 selectedRect.x -= (selectedRect.width - oldWidth) / 2; 292 } 293 if (selectedRect.height != oldHeight) { 294 selectedRect.y -= (selectedRect.height - oldHeight) / 2; 295 } 296 checkVisibleRectSize(image, selectedRect); 297 checkVisibleRectPos(image, selectedRect); 298 synchronized (StreetsideImageDisplay.this) { 299 visibleRect = selectedRect; 300 } 301 selectedRect = null; 302 StreetsideImageDisplay.this.repaint(); 303 } 304 } 305 306 @Override 307 public void mouseEntered(MouseEvent e) { 308 // Do nothing, method is enforced by MouseListener 309 } 310 311 @Override 312 public void mouseExited(MouseEvent e) { 313 // Do nothing, method is enforced by MouseListener 314 } 315 316 @Override 317 public void mouseMoved(MouseEvent e) { 318 // Do nothing, method is enforced by MouseListener 319 } 320 321 private void checkPointInVisibleRect(Point p, Rectangle visibleRect) { 322 if (p.x < visibleRect.x) { 323 p.x = visibleRect.x; 324 } 325 if (p.x > visibleRect.x + visibleRect.width) { 326 p.x = visibleRect.x + visibleRect.width; 327 } 328 if (p.y < visibleRect.y) { 329 p.y = visibleRect.y; 330 } 331 if (p.y > visibleRect.y + visibleRect.height) { 332 p.y = visibleRect.y + visibleRect.height; 333 } 334 } 335 } 336 337 /** 338 * Main constructor. 339 */ 340 public StreetsideImageDisplay() { 341 ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener(); 342 addMouseListener(mouseListener); 343 addMouseWheelListener(mouseListener); 344 addMouseMotionListener(mouseListener); 345 346 StreetsideProperties.SHOW_DETECTED_SIGNS.addListener(valChanged -> repaint()); 347 } 348 349 /** 350 * Sets a new picture to be displayed. 351 * 352 * @param image The picture to be displayed. 353 * @param detections image detections 354 */ 355 public void setImage(BufferedImage image, Collection<ImageDetection> detections) { 356 synchronized (this) { 357 this.image = image; 358 this.detections.clear(); 359 if (detections != null) { 360 this.detections.addAll(detections); 361 } 362 selectedRect = null; 363 if (image != null) 364 visibleRect = new Rectangle(0, 0, image.getWidth(null), 365 image.getHeight(null)); 366 } 367 repaint(); 368 } 369 370 /** 371 * Returns the picture that is being displayed 372 * 373 * @return The picture that is being displayed. 374 */ 375 public BufferedImage getImage() { 376 return image; 377 } 378 379 /** 380 * Paints the visible part of the picture. 381 */ 382 @Override 383 public void paintComponent(Graphics g) { 384 Image image; 385 Rectangle visibleRect; 386 synchronized (this) { 387 image = this.image; 388 visibleRect = this.visibleRect; 389 } 390 if (image == null) { 391 g.setColor(Color.black); 392 String noImageStr = StreetsideLayer.hasInstance() ? tr("No image selected") : tr("Press \"{0}\" to download images", StreetsideDownloadAction.SHORTCUT.getKeyText()); 393 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds( 394 noImageStr, g); 395 Dimension size = getSize(); 396 g.drawString(noImageStr, 397 (int) ((size.width - noImageSize.getWidth()) / 2), 398 (int) ((size.height - noImageSize.getHeight()) / 2)); 399 } else { 400 Rectangle target = calculateDrawImageRectangle(visibleRect); 401 g.drawImage(image, target.x, target.y, target.x + target.width, target.y 402 + target.height, visibleRect.x, visibleRect.y, visibleRect.x 403 + visibleRect.width, visibleRect.y + visibleRect.height, null); 404 if (selectedRect != null) { 405 Point topLeft = img2compCoord(visibleRect, selectedRect.x, 406 selectedRect.y); 407 Point bottomRight = img2compCoord(visibleRect, selectedRect.x 408 + selectedRect.width, selectedRect.y + selectedRect.height); 409 g.setColor(new Color(128, 128, 128, 180)); 410 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 411 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 412 g.fillRect(bottomRight.x, target.y, target.x + target.width 413 - bottomRight.x, target.height); 414 g.fillRect(target.x, bottomRight.y, target.width, target.y 415 + target.height - bottomRight.y); 416 g.setColor(Color.black); 417 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, 418 bottomRight.y - topLeft.y); 419 } 420 421 if (StreetsideProperties.SHOW_DETECTED_SIGNS.get()) { 422 Point upperLeft = img2compCoord(visibleRect, 0, 0); 423 Point lowerRight = img2compCoord(visibleRect, getImage().getWidth(), getImage().getHeight()); 424 425 // Transformation, which can convert you a Shape relative to the unit square to a Shape relative to the Component 426 AffineTransform unit2compTransform = AffineTransform.getTranslateInstance(upperLeft.getX(), upperLeft.getY()); 427 unit2compTransform.concatenate(AffineTransform.getScaleInstance(lowerRight.getX() - upperLeft.getX(), lowerRight.getY() - upperLeft.getY())); 428 429 final Graphics2D g2d = (Graphics2D) g; 430 g2d.setStroke(new BasicStroke(2)); 431 for (ImageDetection d : detections) { 432 final Shape shape = d.getShape().createTransformedShape(unit2compTransform); 433 g2d.setColor(d.isTrafficSign() ? StreetsideColorScheme.IMAGEDETECTION_TRAFFICSIGN : StreetsideColorScheme.IMAGEDETECTION_UNKNOWN); 434 g2d.draw(shape); 435 if (d.isTrafficSign()) { 436 g2d.drawImage( 437 MapObject.getIcon(d.getValue()).getImage(), 438 shape.getBounds().x, shape.getBounds().y, 439 shape.getBounds().width, shape.getBounds().height, 440 null 441 ); 442 } 443 } 444 } 445 } 446 } 447 448 private Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) { 449 Rectangle drawRect = calculateDrawImageRectangle(visibleRect); 450 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) 451 / visibleRect.width, drawRect.y 452 + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height); 453 } 454 455 private Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) { 456 Rectangle drawRect = calculateDrawImageRectangle(visibleRect); 457 return new Point( 458 visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width, 459 visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height 460 ); 461 } 462 463 private static Point getCenterImgCoord(Rectangle visibleRect) { 464 return new Point(visibleRect.x + visibleRect.width / 2, visibleRect.y + visibleRect.height / 2); 465 } 466 467 private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) { 468 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, getSize().width, getSize().height)); 469 } 470 471 /** 472 * calculateDrawImageRectangle 473 * 474 * @param imgRect 475 * the part of the image that should be drawn (in image coordinates) 476 * @param compRect 477 * the part of the component where the image should be drawn (in 478 * component coordinates) 479 * @return the part of compRect with the same width/height ratio as the image 480 */ 481 private static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) { 482 int x = 0; 483 int y = 0; 484 int w = compRect.width; 485 int h = compRect.height; 486 int wFact = w * imgRect.height; 487 int hFact = h * imgRect.width; 488 if (wFact != hFact) { 489 if (wFact > hFact) { 490 w = hFact / imgRect.height; 491 x = (compRect.width - w) / 2; 492 } else { 493 h = wFact / imgRect.width; 494 y = (compRect.height - h) / 2; 495 } 496 } 497 return new Rectangle(x + compRect.x, y + compRect.y, w, h); 498 } 499 500 /** 501 * Zooms to 1:1 and, if it is already in 1:1, to best fit. 502 */ 503 public void zoomBestFitOrOne() { 504 Image image; 505 Rectangle visibleRect; 506 synchronized (this) { 507 image = this.image; 508 visibleRect = this.visibleRect; 509 } 510 if (image == null) 511 return; 512 if (visibleRect.width != image.getWidth(null) 513 || visibleRect.height != image.getHeight(null)) { 514 // The display is not at best fit. => Zoom to best fit 515 visibleRect = new Rectangle(0, 0, image.getWidth(null), 516 image.getHeight(null)); 517 } else { 518 // The display is at best fit => zoom to 1:1 519 Point center = getCenterImgCoord(visibleRect); 520 visibleRect = new Rectangle(center.x - getWidth() / 2, center.y 521 - getHeight() / 2, getWidth(), getHeight()); 522 checkVisibleRectPos(image, visibleRect); 523 } 524 synchronized (this) { 525 this.visibleRect = visibleRect; 526 } 527 repaint(); 528 } 529 530 private static void checkVisibleRectPos(Image image, Rectangle visibleRect) { 531 if (visibleRect.x < 0) { 532 visibleRect.x = 0; 533 } 534 if (visibleRect.y < 0) { 535 visibleRect.y = 0; 536 } 537 if (visibleRect.x + visibleRect.width > image.getWidth(null)) { 538 visibleRect.x = image.getWidth(null) - visibleRect.width; 539 } 540 if (visibleRect.y + visibleRect.height > image.getHeight(null)) { 541 visibleRect.y = image.getHeight(null) - visibleRect.height; 542 } 543 } 544 545 private static void checkVisibleRectSize(Image image, Rectangle visibleRect) { 546 if (visibleRect.width > image.getWidth(null)) { 547 visibleRect.width = image.getWidth(null); 548 } 549 if (visibleRect.height > image.getHeight(null)) { 550 visibleRect.height = image.getHeight(null); 551 } 552 } 553}