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