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, 18 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.