source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java@ 18947

Last change on this file since 18947 was 18947, checked in by GerdP, 4 months ago

fix memory leak, ImgDisplay.destroy() was only called when at least one image was viewed

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