001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer.geoimage; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Dimension; 008import java.awt.FontMetrics; 009import java.awt.Graphics; 010import java.awt.Graphics2D; 011import java.awt.Image; 012import java.awt.MediaTracker; 013import java.awt.Point; 014import java.awt.Rectangle; 015import java.awt.Toolkit; 016import java.awt.event.MouseEvent; 017import java.awt.event.MouseListener; 018import java.awt.event.MouseMotionListener; 019import java.awt.event.MouseWheelEvent; 020import java.awt.event.MouseWheelListener; 021import java.awt.geom.AffineTransform; 022import java.awt.geom.Rectangle2D; 023import java.awt.image.BufferedImage; 024import java.io.File; 025 026import javax.swing.JComponent; 027 028import org.openstreetmap.josm.Main; 029import org.openstreetmap.josm.tools.ExifReader; 030 031 032public class ImageDisplay extends JComponent { 033 034 /** The file that is currently displayed */ 035 private File file; 036 037 /** The image currently displayed */ 038 private transient Image image; 039 040 /** The image currently displayed */ 041 private boolean errorLoading; 042 043 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated 044 * each time the zoom is modified */ 045 private Rectangle visibleRect; 046 047 /** When a selection is done, the rectangle of the selection (in image coordinates) */ 048 private Rectangle selectedRect; 049 050 /** The tracker to load the images */ 051 private MediaTracker tracker = new MediaTracker(this); 052 053 private String osdText; 054 055 private static final int DRAG_BUTTON = Main.pref.getBoolean("geoimage.agpifo-style-drag-and-zoom", false) ? 1 : 3; 056 private static final int ZOOM_BUTTON = DRAG_BUTTON == 1 ? 3 : 1; 057 058 /** The thread that reads the images. */ 059 private class LoadImageRunnable implements Runnable { 060 061 private File file; 062 private int orientation; 063 064 LoadImageRunnable(File file, Integer orientation) { 065 this.file = file; 066 this.orientation = orientation == null ? -1 : orientation; 067 } 068 069 @Override 070 public void run() { 071 Image img = Toolkit.getDefaultToolkit().createImage(file.getPath()); 072 tracker.addImage(img, 1); 073 074 // Wait for the end of loading 075 while (!tracker.checkID(1, true)) { 076 if (this.file != ImageDisplay.this.file) { 077 // The file has changed 078 tracker.removeImage(img); 079 return; 080 } 081 try { 082 Thread.sleep(5); 083 } catch (InterruptedException e) { 084 Main.warn("InterruptedException in "+getClass().getSimpleName()+" while loading image "+file.getPath()); 085 } 086 } 087 088 boolean error = tracker.isErrorID(1); 089 if (img.getWidth(null) < 0 || img.getHeight(null) < 0) { 090 error = true; 091 } 092 093 synchronized (ImageDisplay.this) { 094 if (this.file != ImageDisplay.this.file) { 095 // The file has changed 096 tracker.removeImage(img); 097 return; 098 } 099 100 if (!error) { 101 ImageDisplay.this.image = img; 102 visibleRect = new Rectangle(0, 0, img.getWidth(null), img.getHeight(null)); 103 104 final int w = (int) visibleRect.getWidth(); 105 final int h = (int) visibleRect.getHeight(); 106 107 if (ExifReader.orientationNeedsCorrection(orientation)) { 108 final int hh, ww; 109 if (ExifReader.orientationSwitchesDimensions(orientation)) { 110 ww = h; 111 hh = w; 112 } else { 113 ww = w; 114 hh = h; 115 } 116 final BufferedImage rot = new BufferedImage(ww, hh, BufferedImage.TYPE_INT_RGB); 117 final AffineTransform xform = ExifReader.getRestoreOrientationTransform(orientation, w, h); 118 final Graphics2D g = rot.createGraphics(); 119 g.drawImage(image, xform, null); 120 g.dispose(); 121 122 visibleRect.setSize(ww, hh); 123 image.flush(); 124 ImageDisplay.this.image = rot; 125 } 126 } 127 128 selectedRect = null; 129 errorLoading = error; 130 } 131 tracker.removeImage(img); 132 ImageDisplay.this.repaint(); 133 } 134 } 135 136 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener { 137 138 private boolean mouseIsDragging; 139 private long lastTimeForMousePoint; 140 private Point mousePointInImg; 141 142 /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor 143 * at the same place */ 144 @Override 145 public void mouseWheelMoved(MouseWheelEvent e) { 146 File file; 147 Image image; 148 Rectangle visibleRect; 149 150 synchronized (ImageDisplay.this) { 151 file = ImageDisplay.this.file; 152 image = ImageDisplay.this.image; 153 visibleRect = ImageDisplay.this.visibleRect; 154 } 155 156 mouseIsDragging = false; 157 selectedRect = null; 158 159 if (image == null) 160 return; 161 162 // Calculate the mouse cursor position in image coordinates, so that we can center the zoom 163 // on that mouse position. 164 // To avoid issues when the user tries to zoom in on the image borders, this point is not calculated 165 // again if there was less than 1.5seconds since the last event. 166 if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) { 167 lastTimeForMousePoint = e.getWhen(); 168 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY()); 169 } 170 171 // Applicate the zoom to the visible rectangle in image coordinates 172 if (e.getWheelRotation() > 0) { 173 visibleRect.width = visibleRect.width * 3 / 2; 174 visibleRect.height = visibleRect.height * 3 / 2; 175 } else { 176 visibleRect.width = visibleRect.width * 2 / 3; 177 visibleRect.height = visibleRect.height * 2 / 3; 178 } 179 180 // Check that the zoom doesn't exceed 2:1 181 if (visibleRect.width < getSize().width / 2) { 182 visibleRect.width = getSize().width / 2; 183 } 184 if (visibleRect.height < getSize().height / 2) { 185 visibleRect.height = getSize().height / 2; 186 } 187 188 // Set the same ratio for the visible rectangle and the display area 189 int hFact = visibleRect.height * getSize().width; 190 int wFact = visibleRect.width * getSize().height; 191 if (hFact > wFact) { 192 visibleRect.width = hFact / getSize().height; 193 } else { 194 visibleRect.height = wFact / getSize().width; 195 } 196 197 // The size of the visible rectangle is limited by the image size. 198 checkVisibleRectSize(image, visibleRect); 199 200 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image. 201 Rectangle drawRect = calculateDrawImageRectangle(visibleRect); 202 visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width; 203 visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height; 204 205 // The position is also limited by the image size 206 checkVisibleRectPos(image, visibleRect); 207 208 synchronized (ImageDisplay.this) { 209 if (ImageDisplay.this.file == file) { 210 ImageDisplay.this.visibleRect = visibleRect; 211 } 212 } 213 ImageDisplay.this.repaint(); 214 } 215 216 /** Center the display on the point that has been clicked */ 217 @Override 218 public void mouseClicked(MouseEvent e) { 219 // Move the center to the clicked point. 220 File file; 221 Image image; 222 Rectangle visibleRect; 223 224 synchronized (ImageDisplay.this) { 225 file = ImageDisplay.this.file; 226 image = ImageDisplay.this.image; 227 visibleRect = ImageDisplay.this.visibleRect; 228 } 229 230 if (image == null) 231 return; 232 233 if (e.getButton() != DRAG_BUTTON) 234 return; 235 236 // Calculate the translation to set the clicked point the center of the view. 237 Point click = comp2imgCoord(visibleRect, e.getX(), e.getY()); 238 Point center = getCenterImgCoord(visibleRect); 239 240 visibleRect.x += click.x - center.x; 241 visibleRect.y += click.y - center.y; 242 243 checkVisibleRectPos(image, visibleRect); 244 245 synchronized (ImageDisplay.this) { 246 if (ImageDisplay.this.file == file) { 247 ImageDisplay.this.visibleRect = visibleRect; 248 } 249 } 250 ImageDisplay.this.repaint(); 251 } 252 253 /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of 254 * a picture part) */ 255 @Override 256 public void mousePressed(MouseEvent e) { 257 if (image == null) { 258 mouseIsDragging = false; 259 selectedRect = null; 260 return; 261 } 262 263 Image image; 264 Rectangle visibleRect; 265 266 synchronized (ImageDisplay.this) { 267 image = ImageDisplay.this.image; 268 visibleRect = ImageDisplay.this.visibleRect; 269 } 270 271 if (image == null) 272 return; 273 274 if (e.getButton() == DRAG_BUTTON) { 275 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY()); 276 mouseIsDragging = true; 277 selectedRect = null; 278 } else if (e.getButton() == ZOOM_BUTTON) { 279 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY()); 280 checkPointInVisibleRect(mousePointInImg, visibleRect); 281 mouseIsDragging = false; 282 selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0); 283 ImageDisplay.this.repaint(); 284 } else { 285 mouseIsDragging = false; 286 selectedRect = null; 287 } 288 } 289 290 @Override 291 public void mouseDragged(MouseEvent e) { 292 if (!mouseIsDragging && selectedRect == null) 293 return; 294 295 File file; 296 Image image; 297 Rectangle visibleRect; 298 299 synchronized (ImageDisplay.this) { 300 file = ImageDisplay.this.file; 301 image = ImageDisplay.this.image; 302 visibleRect = ImageDisplay.this.visibleRect; 303 } 304 305 if (image == null) { 306 mouseIsDragging = false; 307 selectedRect = null; 308 return; 309 } 310 311 if (mouseIsDragging) { 312 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY()); 313 visibleRect.x += mousePointInImg.x - p.x; 314 visibleRect.y += mousePointInImg.y - p.y; 315 checkVisibleRectPos(image, visibleRect); 316 synchronized (ImageDisplay.this) { 317 if (ImageDisplay.this.file == file) { 318 ImageDisplay.this.visibleRect = visibleRect; 319 } 320 } 321 ImageDisplay.this.repaint(); 322 323 } else if (selectedRect != null) { 324 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY()); 325 checkPointInVisibleRect(p, visibleRect); 326 Rectangle rect = new Rectangle( 327 p.x < mousePointInImg.x ? p.x : mousePointInImg.x, 328 p.y < mousePointInImg.y ? p.y : mousePointInImg.y, 329 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x, 330 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y); 331 checkVisibleRectSize(image, rect); 332 checkVisibleRectPos(image, rect); 333 ImageDisplay.this.selectedRect = rect; 334 ImageDisplay.this.repaint(); 335 } 336 337 } 338 339 @Override 340 public void mouseReleased(MouseEvent e) { 341 if (!mouseIsDragging && selectedRect == null) 342 return; 343 344 File file; 345 Image image; 346 347 synchronized (ImageDisplay.this) { 348 file = ImageDisplay.this.file; 349 image = ImageDisplay.this.image; 350 } 351 352 if (image == null) { 353 mouseIsDragging = false; 354 selectedRect = null; 355 return; 356 } 357 358 if (mouseIsDragging) { 359 mouseIsDragging = false; 360 361 } else if (selectedRect != null) { 362 int oldWidth = selectedRect.width; 363 int oldHeight = selectedRect.height; 364 365 // Check that the zoom doesn't exceed 2:1 366 if (selectedRect.width < getSize().width / 2) { 367 selectedRect.width = getSize().width / 2; 368 } 369 if (selectedRect.height < getSize().height / 2) { 370 selectedRect.height = getSize().height / 2; 371 } 372 373 // Set the same ratio for the visible rectangle and the display area 374 int hFact = selectedRect.height * getSize().width; 375 int wFact = selectedRect.width * getSize().height; 376 if (hFact > wFact) { 377 selectedRect.width = hFact / getSize().height; 378 } else { 379 selectedRect.height = wFact / getSize().width; 380 } 381 382 // Keep the center of the selection 383 if (selectedRect.width != oldWidth) { 384 selectedRect.x -= (selectedRect.width - oldWidth) / 2; 385 } 386 if (selectedRect.height != oldHeight) { 387 selectedRect.y -= (selectedRect.height - oldHeight) / 2; 388 } 389 390 checkVisibleRectSize(image, selectedRect); 391 checkVisibleRectPos(image, selectedRect); 392 393 synchronized (ImageDisplay.this) { 394 if (file == ImageDisplay.this.file) { 395 ImageDisplay.this.visibleRect = selectedRect; 396 } 397 } 398 selectedRect = null; 399 ImageDisplay.this.repaint(); 400 } 401 } 402 403 @Override 404 public void mouseEntered(MouseEvent e) { 405 } 406 407 @Override 408 public void mouseExited(MouseEvent e) { 409 } 410 411 @Override 412 public void mouseMoved(MouseEvent e) { 413 } 414 415 private void checkPointInVisibleRect(Point p, Rectangle visibleRect) { 416 if (p.x < visibleRect.x) { 417 p.x = visibleRect.x; 418 } 419 if (p.x > visibleRect.x + visibleRect.width) { 420 p.x = visibleRect.x + visibleRect.width; 421 } 422 if (p.y < visibleRect.y) { 423 p.y = visibleRect.y; 424 } 425 if (p.y > visibleRect.y + visibleRect.height) { 426 p.y = visibleRect.y + visibleRect.height; 427 } 428 } 429 } 430 431 public ImageDisplay() { 432 ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener(); 433 addMouseListener(mouseListener); 434 addMouseWheelListener(mouseListener); 435 addMouseMotionListener(mouseListener); 436 } 437 438 public void setImage(File file, Integer orientation) { 439 synchronized (this) { 440 this.file = file; 441 image = null; 442 selectedRect = null; 443 errorLoading = false; 444 } 445 repaint(); 446 if (file != null) { 447 new Thread(new LoadImageRunnable(file, orientation), LoadImageRunnable.class.getName()).start(); 448 } 449 } 450 451 public void setOsdText(String text) { 452 this.osdText = text; 453 repaint(); 454 } 455 456 @Override 457 public void paintComponent(Graphics g) { 458 Image image; 459 File file; 460 Rectangle visibleRect; 461 boolean errorLoading; 462 463 synchronized (this) { 464 image = this.image; 465 file = this.file; 466 visibleRect = this.visibleRect; 467 errorLoading = this.errorLoading; 468 } 469 470 if (file == null) { 471 g.setColor(Color.black); 472 String noImageStr = tr("No image"); 473 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g); 474 Dimension size = getSize(); 475 g.drawString(noImageStr, 476 (int) ((size.width - noImageSize.getWidth()) / 2), 477 (int) ((size.height - noImageSize.getHeight()) / 2)); 478 } else if (image == null) { 479 g.setColor(Color.black); 480 String loadingStr; 481 if (!errorLoading) { 482 loadingStr = tr("Loading {0}", file.getName()); 483 } else { 484 loadingStr = tr("Error on file {0}", file.getName()); 485 } 486 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g); 487 Dimension size = getSize(); 488 g.drawString(loadingStr, 489 (int) ((size.width - noImageSize.getWidth()) / 2), 490 (int) ((size.height - noImageSize.getHeight()) / 2)); 491 } else { 492 Rectangle target = calculateDrawImageRectangle(visibleRect); 493 g.drawImage(image, 494 target.x, target.y, target.x + target.width, target.y + target.height, 495 visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height, 496 null); 497 if (selectedRect != null) { 498 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y); 499 Point bottomRight = img2compCoord(visibleRect, 500 selectedRect.x + selectedRect.width, 501 selectedRect.y + selectedRect.height); 502 g.setColor(new Color(128, 128, 128, 180)); 503 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 504 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 505 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height); 506 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y); 507 g.setColor(Color.black); 508 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); 509 } 510 if (errorLoading) { 511 String loadingStr = tr("Error on file {0}", file.getName()); 512 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g); 513 Dimension size = getSize(); 514 g.drawString(loadingStr, 515 (int) ((size.width - noImageSize.getWidth()) / 2), 516 (int) ((size.height - noImageSize.getHeight()) / 2)); 517 } 518 if (osdText != null) { 519 FontMetrics metrics = g.getFontMetrics(g.getFont()); 520 int ascent = metrics.getAscent(); 521 Color bkground = new Color(255, 255, 255, 128); 522 int lastPos = 0; 523 int pos = osdText.indexOf('\n'); 524 int x = 3; 525 int y = 3; 526 String line; 527 while (pos > 0) { 528 line = osdText.substring(lastPos, pos); 529 Rectangle2D lineSize = metrics.getStringBounds(line, g); 530 g.setColor(bkground); 531 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 532 g.setColor(Color.black); 533 g.drawString(line, x, y + ascent); 534 y += (int) lineSize.getHeight(); 535 lastPos = pos + 1; 536 pos = osdText.indexOf('\n', lastPos); 537 } 538 539 line = osdText.substring(lastPos); 540 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g); 541 g.setColor(bkground); 542 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 543 g.setColor(Color.black); 544 g.drawString(line, x, y + ascent); 545 } 546 } 547 } 548 549 private Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) { 550 Rectangle drawRect = calculateDrawImageRectangle(visibleRect); 551 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width, 552 drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height); 553 } 554 555 private Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) { 556 Rectangle drawRect = calculateDrawImageRectangle(visibleRect); 557 return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width, 558 visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height); 559 } 560 561 private static Point getCenterImgCoord(Rectangle visibleRect) { 562 return new Point(visibleRect.x + visibleRect.width / 2, 563 visibleRect.y + visibleRect.height / 2); 564 } 565 566 private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) { 567 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, getSize().width, getSize().height)); 568 } 569 570 /** 571 * calculateDrawImageRectangle 572 * 573 * @param imgRect the part of the image that should be drawn (in image coordinates) 574 * @param compRect the part of the component where the image should be drawn (in component coordinates) 575 * @return the part of compRect with the same width/height ratio as the image 576 */ 577 static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) { 578 int x, y, w, h; 579 x = 0; 580 y = 0; 581 w = compRect.width; 582 h = compRect.height; 583 584 int wFact = w * imgRect.height; 585 int hFact = h * imgRect.width; 586 if (wFact != hFact) { 587 if (wFact > hFact) { 588 w = hFact / imgRect.height; 589 x = (compRect.width - w) / 2; 590 } else { 591 h = wFact / imgRect.width; 592 y = (compRect.height - h) / 2; 593 } 594 } 595 return new Rectangle(x + compRect.x, y + compRect.y, w, h); 596 } 597 598 public void zoomBestFitOrOne() { 599 File file; 600 Image image; 601 Rectangle visibleRect; 602 603 synchronized (this) { 604 file = ImageDisplay.this.file; 605 image = ImageDisplay.this.image; 606 visibleRect = ImageDisplay.this.visibleRect; 607 } 608 609 if (image == null) 610 return; 611 612 if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) { 613 // The display is not at best fit. => Zoom to best fit 614 visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null)); 615 616 } else { 617 // The display is at best fit => zoom to 1:1 618 Point center = getCenterImgCoord(visibleRect); 619 visibleRect = new Rectangle(center.x - getWidth() / 2, center.y - getHeight() / 2, 620 getWidth(), getHeight()); 621 checkVisibleRectPos(image, visibleRect); 622 } 623 624 synchronized (this) { 625 if (file == this.file) { 626 this.visibleRect = visibleRect; 627 } 628 } 629 repaint(); 630 } 631 632 private static void checkVisibleRectPos(Image image, Rectangle visibleRect) { 633 if (visibleRect.x < 0) { 634 visibleRect.x = 0; 635 } 636 if (visibleRect.y < 0) { 637 visibleRect.y = 0; 638 } 639 if (visibleRect.x + visibleRect.width > image.getWidth(null)) { 640 visibleRect.x = image.getWidth(null) - visibleRect.width; 641 } 642 if (visibleRect.y + visibleRect.height > image.getHeight(null)) { 643 visibleRect.y = image.getHeight(null) - visibleRect.height; 644 } 645 } 646 647 private static void checkVisibleRectSize(Image image, Rectangle visibleRect) { 648 if (visibleRect.width > image.getWidth(null)) { 649 visibleRect.width = image.getWidth(null); 650 } 651 if (visibleRect.height > image.getHeight(null)) { 652 visibleRect.height = image.getHeight(null); 653 } 654 } 655}