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.Point; 013import java.awt.Rectangle; 014import java.awt.RenderingHints; 015import java.awt.event.ComponentEvent; 016import java.awt.event.MouseAdapter; 017import java.awt.event.MouseEvent; 018import java.awt.event.MouseWheelEvent; 019import java.awt.geom.Rectangle2D; 020import java.awt.image.BufferedImage; 021import java.io.IOException; 022import java.util.Objects; 023import java.util.concurrent.Future; 024 025import javax.swing.JComponent; 026import javax.swing.SwingUtilities; 027 028import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 029import org.openstreetmap.josm.data.imagery.street_level.Projections; 030import org.openstreetmap.josm.data.preferences.BooleanProperty; 031import org.openstreetmap.josm.data.preferences.DoubleProperty; 032import org.openstreetmap.josm.data.preferences.IntegerProperty; 033import org.openstreetmap.josm.gui.MainApplication; 034import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable; 035import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.IImageViewer; 036import org.openstreetmap.josm.gui.layer.geoimage.viewers.projections.ImageProjectionRegistry; 037import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 038import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener; 039import org.openstreetmap.josm.gui.util.GuiHelper; 040import org.openstreetmap.josm.gui.util.imagery.Vector3D; 041import org.openstreetmap.josm.spi.preferences.Config; 042import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 043import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 044import org.openstreetmap.josm.tools.Destroyable; 045import org.openstreetmap.josm.tools.ImageProcessor; 046import org.openstreetmap.josm.tools.JosmRuntimeException; 047import org.openstreetmap.josm.tools.Logging; 048import org.openstreetmap.josm.tools.Utils; 049 050/** 051 * GUI component to display an image (photograph). 052 * 053 * Offers basic mouse interaction (zoom, drag) and on-screen text. 054 * @since 2566 055 */ 056public class ImageDisplay extends JComponent implements Destroyable, PreferenceChangedListener, FilterChangeListener { 057 058 /** The current image viewer */ 059 private IImageViewer iImageViewer; 060 061 /** The file that is currently displayed */ 062 private IImageEntry<?> entry; 063 064 /** The previous file that is currently displayed. Cleared on paint. Only used to help improve UI error information. */ 065 private IImageEntry<?> oldEntry; 066 067 /** The image currently displayed */ 068 private transient BufferedImage image; 069 070 /** The image currently displayed after applying {@link #imageProcessor} */ 071 private transient BufferedImage processedImage; 072 073 /** 074 * Process the image before it is being displayed 075 */ 076 private final ImageProcessor imageProcessor; 077 078 /** The image currently displayed */ 079 private boolean errorLoading; 080 081 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated 082 * each time the zoom is modified */ 083 private VisRect visibleRect; 084 085 /** When a selection is done, the rectangle of the selection (in image coordinates) */ 086 private VisRect selectedRect; 087 088 private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener(); 089 090 private String emptyText; 091 private String osdText; 092 093 private static final BooleanProperty AGPIFO_STYLE = 094 new BooleanProperty("geoimage.agpifo-style-drag-and-zoom", false); 095 private static int dragButton; 096 private static int zoomButton; 097 098 /** Alternative to mouse wheel zoom; esp. handy if no mouse wheel is present **/ 099 private static final BooleanProperty ZOOM_ON_CLICK = 100 new BooleanProperty("geoimage.use-mouse-clicks-to-zoom", true); 101 102 /** Zoom factor when click or wheel zooming **/ 103 private static final DoubleProperty ZOOM_STEP = 104 new DoubleProperty("geoimage.zoom-step-factor", 3 / 2.0); 105 106 /** Maximum zoom allowed **/ 107 private static final DoubleProperty MAX_ZOOM = 108 new DoubleProperty("geoimage.maximum-zoom-scale", 2.0); 109 110 /** Maximum width (in pixels) for loading images **/ 111 private static final IntegerProperty MAX_WIDTH = 112 new IntegerProperty("geoimage.maximum-width", 6000); 113 114 /** Show a background for the error text (may be hard on eyes) */ 115 private static final BooleanProperty ERROR_MESSAGE_BACKGROUND = new BooleanProperty("geoimage.message.error.background", false); 116 117 private UpdateImageThread updateImageThreadInstance; 118 119 private class UpdateImageThread extends Thread { 120 private boolean restart; 121 122 @Override 123 public void run() { 124 updateProcessedImage(); 125 if (restart) { 126 restart = false; 127 run(); 128 } 129 } 130 131 public void restart() { 132 restart = true; 133 if (!isAlive()) { 134 restart = false; 135 updateImageThreadInstance = new UpdateImageThread(); 136 updateImageThreadInstance.start(); 137 } 138 } 139 } 140 141 @Override 142 public void preferenceChanged(PreferenceChangeEvent e) { 143 if (e == null || 144 e.getKey().equals(AGPIFO_STYLE.getKey())) { 145 dragButton = AGPIFO_STYLE.get() ? 1 : 3; 146 zoomButton = dragButton == 1 ? 3 : 1; 147 } 148 } 149 150 /** 151 * Manage the visible rectangle of an image with full bounds stored in init. 152 * @since 13127 153 */ 154 public static class VisRect extends Rectangle { 155 private final Rectangle init; 156 157 /** set when this {@code VisRect} is updated by a mouse drag operation and 158 * unset on mouse release **/ 159 public boolean isDragUpdate; 160 161 /** 162 * Constructs a new {@code VisRect}. 163 * @param x the specified X coordinate 164 * @param y the specified Y coordinate 165 * @param width the width of the rectangle 166 * @param height the height of the rectangle 167 */ 168 public VisRect(int x, int y, int width, int height) { 169 super(x, y, width, height); 170 init = new Rectangle(this); 171 } 172 173 /** 174 * Constructs a new {@code VisRect}. 175 * @param x the specified X coordinate 176 * @param y the specified Y coordinate 177 * @param width the width of the rectangle 178 * @param height the height of the rectangle 179 * @param peer share full bounds with this peer {@code VisRect} 180 */ 181 public VisRect(int x, int y, int width, int height, VisRect peer) { 182 super(x, y, width, height); 183 init = peer.init; 184 } 185 186 /** 187 * Constructs a new {@code VisRect} from another one. 188 * @param v rectangle to copy 189 */ 190 public VisRect(VisRect v) { 191 super(v); 192 init = v.init; 193 } 194 195 /** 196 * Constructs a new empty {@code VisRect}. 197 */ 198 public VisRect() { 199 this(0, 0, 0, 0); 200 } 201 202 public boolean isFullView() { 203 return init.equals(this); 204 } 205 206 public boolean isFullView1D() { 207 return (init.x == x && init.width == width) 208 || (init.y == y && init.height == height); 209 } 210 211 public void reset() { 212 setBounds(init); 213 } 214 215 public void checkRectPos() { 216 if (x < 0) { 217 x = 0; 218 } 219 if (y < 0) { 220 y = 0; 221 } 222 if (x + width > init.width) { 223 x = init.width - width; 224 } 225 if (y + height > init.height) { 226 y = init.height - height; 227 } 228 } 229 230 public void checkRectSize() { 231 if (width > init.width) { 232 width = init.width; 233 } 234 if (height > init.height) { 235 height = init.height; 236 } 237 } 238 239 public void checkPointInside(Point p) { 240 if (p.x < x) { 241 p.x = x; 242 } 243 if (p.x > x + width) { 244 p.x = x + width; 245 } 246 if (p.y < y) { 247 p.y = y; 248 } 249 if (p.y > y + height) { 250 p.y = y + height; 251 } 252 } 253 254 @Override 255 public int hashCode() { 256 return 31 * super.hashCode() + Objects.hash(init); 257 } 258 259 @Override 260 public boolean equals(Object obj) { 261 if (this == obj) 262 return true; 263 if (!super.equals(obj) || getClass() != obj.getClass()) 264 return false; 265 VisRect other = (VisRect) obj; 266 return Objects.equals(init, other.init); 267 } 268 } 269 270 /** The thread that reads the images. */ 271 protected class LoadImageRunnable implements Runnable { 272 273 private final IImageEntry<?> entry; 274 275 LoadImageRunnable(IImageEntry<?> entry) { 276 this.entry = entry; 277 } 278 279 @Override 280 public void run() { 281 try { 282 Dimension target = new Dimension(MAX_WIDTH.get(), MAX_WIDTH.get()); 283 BufferedImage img = entry.read(target); 284 if (img == null) { 285 synchronized (ImageDisplay.this) { 286 errorLoading = true; 287 ImageDisplay.this.repaint(); 288 return; 289 } 290 } 291 292 int width = img.getWidth(); 293 int height = img.getHeight(); 294 entry.setWidth(width); 295 entry.setHeight(height); 296 297 synchronized (ImageDisplay.this) { 298 if (this.entry != ImageDisplay.this.entry) { 299 // The file has changed 300 return; 301 } 302 303 ImageDisplay.this.image = img; 304 updateProcessedImage(); 305 // This will clear the loading info box 306 ImageDisplay.this.oldEntry = ImageDisplay.this.entry; 307 visibleRect = getIImageViewer(entry).getDefaultVisibleRectangle(ImageDisplay.this, image); 308 309 selectedRect = null; 310 errorLoading = false; 311 } 312 ImageDisplay.this.repaint(); 313 } catch (IOException ex) { 314 Logging.error(ex); 315 } 316 } 317 } 318 319 private class ImgDisplayMouseListener extends MouseAdapter { 320 321 private MouseEvent lastMouseEvent; 322 private Point mousePointInImg; 323 324 private boolean mouseIsDragging(MouseEvent e) { 325 return (dragButton == 1 && SwingUtilities.isLeftMouseButton(e)) || 326 (dragButton == 2 && SwingUtilities.isMiddleMouseButton(e)) || 327 (dragButton == 3 && SwingUtilities.isRightMouseButton(e)); 328 } 329 330 private boolean mouseIsZoomSelecting(MouseEvent e) { 331 return (zoomButton == 1 && SwingUtilities.isLeftMouseButton(e)) || 332 (zoomButton == 2 && SwingUtilities.isMiddleMouseButton(e)) || 333 (zoomButton == 3 && SwingUtilities.isRightMouseButton(e)); 334 } 335 336 private boolean isAtMaxZoom(Rectangle visibleRect) { 337 return (visibleRect.width == (int) (getSize().width / MAX_ZOOM.get()) || 338 visibleRect.height == (int) (getSize().height / MAX_ZOOM.get())); 339 } 340 341 private void mouseWheelMovedImpl(int x, int y, int rotation, boolean refreshMousePointInImg) { 342 IImageEntry<?> currentEntry; 343 IImageViewer imageViewer; 344 Image currentImage; 345 VisRect currentVisibleRect; 346 347 synchronized (ImageDisplay.this) { 348 currentEntry = ImageDisplay.this.entry; 349 currentImage = ImageDisplay.this.image; 350 currentVisibleRect = ImageDisplay.this.visibleRect; 351 imageViewer = ImageDisplay.this.iImageViewer; 352 } 353 354 selectedRect = null; 355 356 if (currentImage == null) 357 return; 358 359 // Calculate the mouse cursor position in image coordinates to center the zoom. 360 if (refreshMousePointInImg) 361 mousePointInImg = comp2imgCoord(currentVisibleRect, x, y, getSize()); 362 363 // Apply the zoom to the visible rectangle in image coordinates 364 if (rotation > 0) { 365 currentVisibleRect.width = (int) (currentVisibleRect.width * ZOOM_STEP.get()); 366 currentVisibleRect.height = (int) (currentVisibleRect.height * ZOOM_STEP.get()); 367 } else { 368 currentVisibleRect.width = (int) (currentVisibleRect.width / ZOOM_STEP.get()); 369 currentVisibleRect.height = (int) (currentVisibleRect.height / ZOOM_STEP.get()); 370 } 371 372 // Check that the zoom doesn't exceed MAX_ZOOM:1 373 ensureMaxZoom(currentVisibleRect); 374 375 // The size of the visible rectangle is limited by the image size or the viewer implementation. 376 if (imageViewer != null) { 377 imageViewer.checkAndModifyVisibleRectSize(currentImage, currentVisibleRect); 378 } else { 379 currentVisibleRect.checkRectSize(); 380 } 381 382 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image. 383 Rectangle drawRect = calculateDrawImageRectangle(currentVisibleRect, getSize()); 384 currentVisibleRect.x = mousePointInImg.x + ((drawRect.x - x) * currentVisibleRect.width) / drawRect.width; 385 currentVisibleRect.y = mousePointInImg.y + ((drawRect.y - y) * currentVisibleRect.height) / drawRect.height; 386 387 // The position is also limited by the image size 388 currentVisibleRect.checkRectPos(); 389 390 synchronized (ImageDisplay.this) { 391 if (ImageDisplay.this.entry == currentEntry) { 392 ImageDisplay.this.visibleRect = currentVisibleRect; 393 } 394 } 395 ImageDisplay.this.repaint(); 396 } 397 398 /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor 399 * at the same place */ 400 @Override 401 public void mouseWheelMoved(MouseWheelEvent e) { 402 boolean refreshMousePointInImg = false; 403 404 // To avoid issues when the user tries to zoom in on the image borders, this 405 // point is not recalculated as long as e occurs at roughly the same position. 406 if (lastMouseEvent == null || mousePointInImg == null || 407 ((lastMouseEvent.getX()-e.getX())*(lastMouseEvent.getX()-e.getX()) 408 +(lastMouseEvent.getY()-e.getY())*(lastMouseEvent.getY()-e.getY()) > 4*4)) { 409 lastMouseEvent = e; 410 refreshMousePointInImg = true; 411 } 412 413 mouseWheelMovedImpl(e.getX(), e.getY(), e.getWheelRotation(), refreshMousePointInImg); 414 } 415 416 /** Center the display on the point that has been clicked */ 417 @Override 418 public void mouseClicked(MouseEvent e) { 419 // Move the center to the clicked point. 420 IImageEntry<?> currentEntry; 421 Image currentImage; 422 VisRect currentVisibleRect; 423 424 synchronized (ImageDisplay.this) { 425 currentEntry = ImageDisplay.this.entry; 426 currentImage = ImageDisplay.this.image; 427 currentVisibleRect = ImageDisplay.this.visibleRect; 428 } 429 430 if (currentImage == null) 431 return; 432 433 if (ZOOM_ON_CLICK.get()) { 434 // click notions are less coherent than wheel, refresh mousePointInImg on each click 435 lastMouseEvent = null; 436 437 if (mouseIsZoomSelecting(e) && !isAtMaxZoom(currentVisibleRect)) { 438 // zoom in if clicked with the zoom button 439 mouseWheelMovedImpl(e.getX(), e.getY(), -1, true); 440 return; 441 } 442 if (mouseIsDragging(e)) { 443 // zoom out if clicked with the drag button 444 mouseWheelMovedImpl(e.getX(), e.getY(), 1, true); 445 return; 446 } 447 } 448 449 // Calculate the translation to set the clicked point the center of the view. 450 Point click = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 451 Point center = getCenterImgCoord(currentVisibleRect); 452 453 currentVisibleRect.x += click.x - center.x; 454 currentVisibleRect.y += click.y - center.y; 455 456 currentVisibleRect.checkRectPos(); 457 458 synchronized (ImageDisplay.this) { 459 if (ImageDisplay.this.entry == currentEntry) { 460 ImageDisplay.this.visibleRect = currentVisibleRect; 461 } 462 } 463 ImageDisplay.this.repaint(); 464 } 465 466 /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of 467 * a picture part) */ 468 @Override 469 public void mousePressed(MouseEvent e) { 470 Image currentImage; 471 VisRect currentVisibleRect; 472 473 synchronized (ImageDisplay.this) { 474 currentImage = ImageDisplay.this.image; 475 currentVisibleRect = ImageDisplay.this.visibleRect; 476 } 477 478 if (currentImage == null) 479 return; 480 481 selectedRect = null; 482 483 if (mouseIsDragging(e) || mouseIsZoomSelecting(e)) 484 mousePointInImg = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 485 } 486 487 @Override 488 public void mouseDragged(MouseEvent e) { 489 if (!mouseIsDragging(e) && !mouseIsZoomSelecting(e)) 490 return; 491 492 IImageEntry<?> imageEntry; 493 Image currentImage; 494 VisRect currentVisibleRect; 495 496 synchronized (ImageDisplay.this) { 497 imageEntry = ImageDisplay.this.entry; 498 currentImage = ImageDisplay.this.image; 499 currentVisibleRect = ImageDisplay.this.visibleRect; 500 } 501 502 if (currentImage == null) 503 return; 504 505 if (mouseIsDragging(e) && mousePointInImg != null) { 506 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 507 getIImageViewer(entry).mouseDragged(this.mousePointInImg, p, currentVisibleRect); 508 currentVisibleRect.checkRectPos(); 509 synchronized (ImageDisplay.this) { 510 if (ImageDisplay.this.entry == imageEntry) { 511 ImageDisplay.this.visibleRect = currentVisibleRect; 512 } 513 } 514 // We have to update the mousePointInImg for 360 image panning, as otherwise the panning never stops. 515 // This does not work well with the perspective viewer at this time (2021-08-26). 516 boolean is360panning = entry != null && Projections.EQUIRECTANGULAR == entry.getProjectionType(); 517 if (is360panning) { 518 this.mousePointInImg = p; 519 } 520 ImageDisplay.this.repaint(); 521 if (is360panning) { 522 // repaint direction arrow 523 MainApplication.getLayerManager().getLayersOfType(GeoImageLayer.class).forEach(AbstractMapViewPaintable::invalidate); 524 } 525 } 526 527 if (mouseIsZoomSelecting(e) && mousePointInImg != null) { 528 Point p = comp2imgCoord(currentVisibleRect, e.getX(), e.getY(), getSize()); 529 currentVisibleRect.checkPointInside(p); 530 VisRect selectedRectTemp = new VisRect( 531 Math.min(p.x, mousePointInImg.x), 532 Math.min(p.y, mousePointInImg.y), 533 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x, 534 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y, 535 currentVisibleRect); 536 selectedRectTemp.checkRectSize(); 537 selectedRectTemp.checkRectPos(); 538 ImageDisplay.this.selectedRect = selectedRectTemp; 539 ImageDisplay.this.repaint(); 540 } 541 } 542 543 @Override 544 public void mouseReleased(MouseEvent e) { 545 IImageEntry<?> currentEntry; 546 Image currentImage; 547 VisRect currentVisibleRect; 548 549 synchronized (ImageDisplay.this) { 550 currentEntry = ImageDisplay.this.entry; 551 currentImage = ImageDisplay.this.image; 552 currentVisibleRect = ImageDisplay.this.visibleRect; 553 } 554 555 if (currentImage == null) 556 return; 557 558 if (mouseIsDragging(e)) { 559 currentVisibleRect.isDragUpdate = false; 560 } 561 562 if (mouseIsZoomSelecting(e) && selectedRect != null) { 563 int oldWidth = selectedRect.width; 564 int oldHeight = selectedRect.height; 565 566 // Check that the zoom doesn't exceed MAX_ZOOM:1 567 ensureMaxZoom(selectedRect); 568 569 // Keep the center of the selection 570 if (selectedRect.width != oldWidth) { 571 selectedRect.x -= (selectedRect.width - oldWidth) / 2; 572 } 573 if (selectedRect.height != oldHeight) { 574 selectedRect.y -= (selectedRect.height - oldHeight) / 2; 575 } 576 577 selectedRect.checkRectSize(); 578 selectedRect.checkRectPos(); 579 } 580 581 synchronized (ImageDisplay.this) { 582 if (currentEntry == ImageDisplay.this.entry) { 583 if (selectedRect == null) { 584 ImageDisplay.this.visibleRect = currentVisibleRect; 585 } else { 586 ImageDisplay.this.visibleRect.setBounds(selectedRect); 587 selectedRect = null; 588 } 589 } 590 } 591 ImageDisplay.this.repaint(); 592 } 593 } 594 595 /** 596 * Constructs a new {@code ImageDisplay} with no image processor. 597 */ 598 public ImageDisplay() { 599 this(imageObject -> imageObject); 600 } 601 602 /** 603 * Constructs a new {@code ImageDisplay} with a given image processor. 604 * @param imageProcessor image processor 605 * @since 17740 606 */ 607 public ImageDisplay(ImageProcessor imageProcessor) { 608 addMouseListener(imgMouseListener); 609 addMouseWheelListener(imgMouseListener); 610 addMouseMotionListener(imgMouseListener); 611 Config.getPref().addPreferenceChangeListener(this); 612 preferenceChanged(null); 613 this.imageProcessor = imageProcessor; 614 if (imageProcessor instanceof ImageryFilterSettings) { 615 ((ImageryFilterSettings) imageProcessor).addFilterChangeListener(this); 616 } 617 } 618 619 @Override 620 public void destroy() { 621 removeMouseListener(imgMouseListener); 622 removeMouseWheelListener(imgMouseListener); 623 removeMouseMotionListener(imgMouseListener); 624 Config.getPref().removePreferenceChangeListener(this); 625 if (imageProcessor instanceof ImageryFilterSettings) { 626 ((ImageryFilterSettings) imageProcessor).removeFilterChangeListener(this); 627 } 628 } 629 630 /** 631 * Sets a new source image to be displayed by this {@code ImageDisplay}. 632 * @param entry new source image 633 * @return a {@link Future} representing pending completion of the image loading task 634 * @since 18246 (signature) 635 */ 636 public Future<?> setImage(IImageEntry<?> entry) { 637 LoadImageRunnable runnable = setImage0(entry); 638 return runnable != null ? MainApplication.worker.submit(runnable) : null; 639 } 640 641 protected LoadImageRunnable setImage0(IImageEntry<?> entry) { 642 synchronized (this) { 643 this.oldEntry = this.entry; 644 this.entry = entry; 645 if (entry == null) { 646 image = null; 647 updateProcessedImage(); 648 this.oldEntry = null; 649 } 650 errorLoading = false; 651 } 652 repaint(); 653 return entry != null ? new LoadImageRunnable(entry) : null; 654 } 655 656 /** 657 * Set the message displayed when there is no image to display. 658 * By default it display a simple No image 659 * @param emptyText the string to display 660 * @since 15333 661 */ 662 public void setEmptyText(String emptyText) { 663 this.emptyText = emptyText; 664 } 665 666 /** 667 * Sets the On-Screen-Display text. 668 * @param text text to display on top of the image 669 */ 670 public void setOsdText(String text) { 671 if (!text.equals(this.osdText)) { 672 this.osdText = text; 673 repaint(); 674 } 675 } 676 677 @Override 678 public void filterChanged() { 679 if (updateImageThreadInstance != null) { 680 updateImageThreadInstance.restart(); 681 } else { 682 updateImageThreadInstance = new UpdateImageThread(); 683 updateImageThreadInstance.start(); 684 } 685 } 686 687 private void updateProcessedImage() { 688 processedImage = image == null ? null : imageProcessor.process(image); 689 GuiHelper.runInEDT(this::repaint); 690 } 691 692 @Override 693 public void paintComponent(Graphics g) { 694 super.paintComponent(g); 695 696 IImageEntry<?> currentEntry; 697 IImageEntry<?> currentOldEntry; 698 IImageViewer currentImageViewer; 699 BufferedImage currentImage; 700 VisRect currentVisibleRect; 701 boolean currentErrorLoading; 702 703 synchronized (this) { 704 currentImage = this.processedImage; 705 currentEntry = this.entry; 706 currentOldEntry = this.oldEntry; 707 currentVisibleRect = this.visibleRect; 708 currentErrorLoading = this.errorLoading; 709 } 710 711 if (g instanceof Graphics2D) { 712 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 713 } 714 715 Dimension size = getSize(); 716 // Draw the image first, then draw error information 717 if (currentImage != null && (currentEntry != null || currentOldEntry != null)) { 718 currentImageViewer = this.getIImageViewer(currentEntry); 719 Rectangle r = new Rectangle(currentVisibleRect); 720 Rectangle target = calculateDrawImageRectangle(currentVisibleRect, size); 721 722 currentImageViewer.paintImage(g, currentImage, target, r); 723 paintSelectedRect(g, target, currentVisibleRect, size); 724 if (currentErrorLoading && currentEntry != null) { 725 String loadingStr = tr("Error on file {0}", currentEntry.getDisplayName()); 726 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g); 727 g.drawString(loadingStr, (int) ((size.width - noImageSize.getWidth()) / 2), 728 (int) ((size.height - noImageSize.getHeight()) / 2)); 729 } 730 paintOsdText(g); 731 } 732 paintErrorMessage(g, currentEntry, currentOldEntry, currentImage, currentErrorLoading, size); 733 } 734 735 /** 736 * Paint an error message 737 * @param g The graphics to paint on 738 * @param imageEntry The current image entry 739 * @param oldImageEntry The old image entry 740 * @param bufferedImage The image being painted 741 * @param currentErrorLoading If there was an error loading the image 742 * @param size The size of the component 743 */ 744 private void paintErrorMessage(Graphics g, IImageEntry<?> imageEntry, IImageEntry<?> oldImageEntry, 745 BufferedImage bufferedImage, boolean currentErrorLoading, Dimension size) { 746 final String errorMessage; 747 // If the new entry is null, then there is no image. 748 if (imageEntry == null) { 749 if (emptyText == null) { 750 emptyText = tr("No image"); 751 } 752 errorMessage = emptyText; 753 } else if (bufferedImage == null || !Objects.equals(imageEntry, oldImageEntry)) { 754 // The image is not necessarily null when loading anymore. If the oldEntry is not the same as the new entry, 755 // we are probably still loading the image. (oldEntry gets set to entry when the image finishes loading). 756 if (!currentErrorLoading) { 757 errorMessage = tr("Loading {0}", imageEntry.getDisplayName()); 758 } else { 759 errorMessage = tr("Error on file {0}", imageEntry.getDisplayName()); 760 } 761 } else { 762 errorMessage = null; 763 } 764 if (!Utils.isBlank(errorMessage)) { 765 Rectangle2D errorStringSize = g.getFontMetrics(g.getFont()).getStringBounds(errorMessage, g); 766 if (Boolean.TRUE.equals(ERROR_MESSAGE_BACKGROUND.get())) { 767 int height = g.getFontMetrics().getHeight(); 768 int descender = g.getFontMetrics().getDescent(); 769 g.setColor(getBackground()); 770 int width = (int) (errorStringSize.getWidth() * 1); 771 // top-left of text 772 int tlx = (int) ((size.getWidth() - errorStringSize.getWidth()) / 2); 773 int tly = (int) ((size.getHeight() - 3 * errorStringSize.getHeight()) / 2 + descender); 774 g.fillRect(tlx, tly, width, height); 775 } 776 777 // lower-left of text 778 int llx = (int) ((size.width - errorStringSize.getWidth()) / 2); 779 int lly = (int) ((size.height - errorStringSize.getHeight()) / 2); 780 g.setColor(getForeground()); 781 g.drawString(errorMessage, llx, lly); 782 } 783 } 784 785 /** 786 * Paint OSD text 787 * @param g The graphics to paint on 788 */ 789 private void paintOsdText(Graphics g) { 790 if (osdText != null) { 791 FontMetrics metrics = g.getFontMetrics(g.getFont()); 792 int ascent = metrics.getAscent(); 793 Color bkground = new Color(255, 255, 255, 128); 794 int lastPos = 0; 795 int pos = osdText.indexOf('\n'); 796 int x = 3; 797 int y = 3; 798 String line; 799 while (pos > 0) { 800 line = osdText.substring(lastPos, pos); 801 Rectangle2D lineSize = metrics.getStringBounds(line, g); 802 g.setColor(bkground); 803 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 804 g.setColor(Color.black); 805 g.drawString(line, x, y + ascent); 806 y += (int) lineSize.getHeight(); 807 lastPos = pos + 1; 808 pos = osdText.indexOf('\n', lastPos); 809 } 810 811 line = osdText.substring(lastPos); 812 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g); 813 g.setColor(bkground); 814 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight()); 815 g.setColor(Color.black); 816 g.drawString(line, x, y + ascent); 817 } 818 } 819 820 /** 821 * Paint the selected rectangle 822 * @param g The graphics to paint on 823 * @param target The target area (i.e., the selection) 824 * @param visibleRectTemp The current visible rect 825 * @param size The size of the component 826 */ 827 private void paintSelectedRect(Graphics g, Rectangle target, VisRect visibleRectTemp, Dimension size) { 828 if (selectedRect != null) { 829 Point topLeft = img2compCoord(visibleRectTemp, selectedRect.x, selectedRect.y, size); 830 Point bottomRight = img2compCoord(visibleRectTemp, 831 selectedRect.x + selectedRect.width, 832 selectedRect.y + selectedRect.height, size); 833 g.setColor(new Color(128, 128, 128, 180)); 834 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y); 835 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height); 836 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height); 837 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y); 838 g.setColor(Color.black); 839 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); 840 } 841 } 842 843 static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) { 844 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 845 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width, 846 drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height); 847 } 848 849 static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) { 850 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize); 851 Point p = new Point( 852 ((xComp - drawRect.x) * visibleRect.width), 853 ((yComp - drawRect.y) * visibleRect.height)); 854 p.x += (((p.x % drawRect.width) << 1) >= drawRect.width) ? drawRect.width : 0; 855 p.y += (((p.y % drawRect.height) << 1) >= drawRect.height) ? drawRect.height : 0; 856 p.x = visibleRect.x + p.x / drawRect.width; 857 p.y = visibleRect.y + p.y / drawRect.height; 858 return p; 859 } 860 861 static Point getCenterImgCoord(Rectangle visibleRect) { 862 return new Point(visibleRect.x + visibleRect.width / 2, 863 visibleRect.y + visibleRect.height / 2); 864 } 865 866 /** 867 * calculateDrawImageRectangle 868 * 869 * @param visibleRect the part of the image that should be drawn (in image coordinates) 870 * @param compSize the part of the component where the image should be drawn (in component coordinates) 871 * @return the part of compRect with the same width/height ratio as the image 872 */ 873 static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) { 874 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height)); 875 } 876 877 /** 878 * calculateDrawImageRectangle 879 * 880 * @param imgRect the part of the image that should be drawn (in image coordinates) 881 * @param compRect the part of the component where the image should be drawn (in component coordinates) 882 * @return the part of compRect with the same width/height ratio as the image 883 */ 884 static VisRect calculateDrawImageRectangle(VisRect imgRect, Rectangle compRect) { 885 int x = 0; 886 int y = 0; 887 int w = compRect.width; 888 int h = compRect.height; 889 890 int wFact = w * imgRect.height; 891 int hFact = h * imgRect.width; 892 if (wFact != hFact) { 893 if (wFact > hFact) { 894 w = hFact / imgRect.height; 895 x = (compRect.width - w) / 2; 896 } else { 897 h = wFact / imgRect.width; 898 y = (compRect.height - h) / 2; 899 } 900 } 901 902 // overscan to prevent empty edges when zooming in to zoom scales > 2:1 903 if (w > imgRect.width && h > imgRect.height && !imgRect.isFullView1D() && wFact != hFact) { 904 if (wFact > hFact) { 905 w = compRect.width; 906 x = 0; 907 h = wFact / imgRect.width; 908 y = (compRect.height - h) / 2; 909 } else { 910 h = compRect.height; 911 y = 0; 912 w = hFact / imgRect.height; 913 x = (compRect.width - w) / 2; 914 } 915 } 916 917 return new VisRect(x + compRect.x, y + compRect.y, w, h, imgRect); 918 } 919 920 /** 921 * Make the current image either scale to fit inside this component, 922 * or show a portion of image (1:1), if the image size is larger than 923 * the component size. 924 */ 925 public void zoomBestFitOrOne() { 926 IImageEntry<?> currentEntry; 927 Image currentImage; 928 VisRect currentVisibleRect; 929 930 synchronized (this) { 931 currentEntry = this.entry; 932 currentImage = this.image; 933 currentVisibleRect = this.visibleRect; 934 } 935 936 if (currentImage == null) 937 return; 938 939 if (currentVisibleRect.width != currentImage.getWidth(null) || currentVisibleRect.height != currentImage.getHeight(null)) { 940 // The display is not at best fit. => Zoom to best fit 941 currentVisibleRect.reset(); 942 } else { 943 // The display is at best fit => zoom to 1:1 944 Point center = getCenterImgCoord(currentVisibleRect); 945 currentVisibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2, 946 getWidth(), getHeight()); 947 currentVisibleRect.checkRectSize(); 948 currentVisibleRect.checkRectPos(); 949 } 950 951 synchronized (this) { 952 if (this.entry == currentEntry) { 953 this.visibleRect = currentVisibleRect; 954 } 955 } 956 repaint(); 957 } 958 959 /** 960 * Get the image viewer for an entry 961 * @param entry The entry to get the viewer for. May be {@code null}. 962 * @return The new image viewer, may be {@code null} 963 */ 964 private IImageViewer getIImageViewer(IImageEntry<?> entry) { 965 IImageViewer imageViewer; 966 IImageEntry<?> imageEntry; 967 synchronized (this) { 968 imageViewer = this.iImageViewer; 969 imageEntry = entry == null ? this.entry : entry; 970 } 971 if (imageEntry == null || (imageViewer != null && imageViewer.getSupportedProjections().contains(imageEntry.getProjectionType()))) { 972 return imageViewer; 973 } 974 try { 975 imageViewer = ImageProjectionRegistry.getViewer(imageEntry.getProjectionType()).getConstructor().newInstance(); 976 } catch (ReflectiveOperationException e) { 977 throw new JosmRuntimeException(e); 978 } 979 synchronized (this) { 980 if (imageEntry.equals(this.entry)) { 981 this.removeComponentListener(this.iImageViewer); 982 this.iImageViewer = imageViewer; 983 imageViewer.componentResized(new ComponentEvent(this, ComponentEvent.COMPONENT_RESIZED)); 984 this.addComponentListener(this.iImageViewer); 985 } 986 } 987 return imageViewer; 988 } 989 990 /** 991 * Get the rotation in the image viewer for an entry 992 * @param entry The entry to get the rotation for. May be {@code null}. 993 * @return the current rotation in the image viewer, or {@code null} 994 * @since 18263 995 */ 996 public Vector3D getRotation(IImageEntry<?> entry) { 997 return entry != null ? getIImageViewer(entry).getRotation() : null; 998 } 999 1000 /** 1001 * Ensure that a rectangle isn't zoomed in too much 1002 * @param rectangle The rectangle to get (typically the visible area) 1003 */ 1004 private void ensureMaxZoom(final Rectangle rectangle) { 1005 if (rectangle.width < getSize().width / MAX_ZOOM.get()) { 1006 rectangle.width = (int) (getSize().width / MAX_ZOOM.get()); 1007 } 1008 if (rectangle.height < getSize().height / MAX_ZOOM.get()) { 1009 rectangle.height = (int) (getSize().height / MAX_ZOOM.get()); 1010 } 1011 1012 // Set the same ratio for the visible rectangle and the display area 1013 int hFact = rectangle.height * getSize().width; 1014 int wFact = rectangle.width * getSize().height; 1015 if (hFact > wFact) { 1016 rectangle.width = hFact / getSize().height; 1017 } else { 1018 rectangle.height = wFact / getSize().width; 1019 } 1020 } 1021 1022 /** 1023 * Update the visible rectangle (ensure zoom does not exceed specified values). 1024 * Specifically only visible for {@link IImageViewer} implementations. 1025 * @since 18246 1026 */ 1027 public void updateVisibleRectangle() { 1028 final VisRect currentVisibleRect; 1029 final Image mouseImage; 1030 final IImageViewer iImageViewer; 1031 synchronized (this) { 1032 currentVisibleRect = this.visibleRect; 1033 mouseImage = this.image; 1034 iImageViewer = this.getIImageViewer(this.entry); 1035 } 1036 if (mouseImage != null && currentVisibleRect != null && iImageViewer != null) { 1037 final Image maxImageSize = iImageViewer.getMaxImageSize(this, mouseImage); 1038 final VisRect maxVisibleRect = new VisRect(0, 0, maxImageSize.getWidth(null), maxImageSize.getHeight(null)); 1039 maxVisibleRect.setRect(currentVisibleRect); 1040 ensureMaxZoom(maxVisibleRect); 1041 1042 maxVisibleRect.checkRectSize(); 1043 synchronized (this) { 1044 this.visibleRect = maxVisibleRect; 1045 } 1046 } 1047 } 1048}