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