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