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

Last change on this file since 16284 was 16284, checked in by simon04, 4 years ago

see #19067 - ImageDisplay: tune logging

  • Property svn:eol-style set to native
File size: 36.8 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.MediaTracker;
13import java.awt.Point;
14import java.awt.Rectangle;
15import java.awt.RenderingHints;
16import java.awt.Toolkit;
17import java.awt.event.MouseEvent;
18import java.awt.event.MouseListener;
19import java.awt.event.MouseMotionListener;
20import java.awt.event.MouseWheelEvent;
21import java.awt.event.MouseWheelListener;
22import java.awt.geom.AffineTransform;
23import java.awt.geom.Rectangle2D;
24import java.awt.image.BufferedImage;
25import java.awt.image.ImageObserver;
26import java.io.File;
27
28import javax.swing.JComponent;
29import javax.swing.SwingUtilities;
30
31import org.openstreetmap.josm.data.preferences.BooleanProperty;
32import org.openstreetmap.josm.data.preferences.DoubleProperty;
33import org.openstreetmap.josm.spi.preferences.Config;
34import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
35import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
36import org.openstreetmap.josm.tools.Destroyable;
37import org.openstreetmap.josm.tools.ExifReader;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Logging;
40
41/**
42 * GUI component to display an image (photograph).
43 *
44 * Offers basic mouse interaction (zoom, drag) and on-screen text.
45 */
46public class ImageDisplay extends JComponent implements Destroyable, PreferenceChangedListener {
47
48 /** The file that is currently displayed */
49 private ImageEntry entry;
50
51 /** The image currently displayed */
52 private transient Image image;
53
54 /** The image currently displayed */
55 private boolean errorLoading;
56
57 /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
58 * each time the zoom is modified */
59 private VisRect visibleRect;
60
61 /** When a selection is done, the rectangle of the selection (in image coordinates) */
62 private VisRect selectedRect;
63
64 /** The tracker to load the images */
65 private final MediaTracker tracker = new MediaTracker(this);
66
67 private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener();
68
69 private String emptyText;
70 private String osdText;
71
72 private static final BooleanProperty AGPIFO_STYLE =
73 new BooleanProperty("geoimage.agpifo-style-drag-and-zoom", false);
74 private static int dragButton;
75 private static int zoomButton;
76
77 /** Alternative to mouse wheel zoom; esp. handy if no mouse wheel is present **/
78 private static final BooleanProperty ZOOM_ON_CLICK =
79 new BooleanProperty("geoimage.use-mouse-clicks-to-zoom", true);
80
81 /** Zoom factor when click or wheel zooming **/
82 private static final DoubleProperty ZOOM_STEP =
83 new DoubleProperty("geoimage.zoom-step-factor", 3 / 2.0);
84
85 /** Maximum zoom allowed **/
86 private static final DoubleProperty MAX_ZOOM =
87 new DoubleProperty("geoimage.maximum-zoom-scale", 2.0);
88
89 /** Use bilinear filtering **/
90 private static final BooleanProperty BILIN_DOWNSAMP =
91 new BooleanProperty("geoimage.bilinear-downsampling-progressive", true);
92 private static final BooleanProperty BILIN_UPSAMP =
93 new BooleanProperty("geoimage.bilinear-upsampling", false);
94 private static double bilinUpper;
95 private static double bilinLower;
96
97 @Override
98 public void preferenceChanged(PreferenceChangeEvent e) {
99 if (e == null ||
100 e.getKey().equals(AGPIFO_STYLE.getKey())) {
101 dragButton = AGPIFO_STYLE.get() ? 1 : 3;
102 zoomButton = dragButton == 1 ? 3 : 1;
103 }
104 if (e == null ||
105 e.getKey().equals(MAX_ZOOM.getKey()) ||
106 e.getKey().equals(BILIN_DOWNSAMP.getKey()) ||
107 e.getKey().equals(BILIN_UPSAMP.getKey())) {
108 bilinUpper = (BILIN_UPSAMP.get() ? 2*MAX_ZOOM.get() : (BILIN_DOWNSAMP.get() ? 0.5 : 0));
109 bilinLower = (BILIN_DOWNSAMP.get() ? 0 : 1);
110 }
111 }
112
113 /**
114 * Manage the visible rectangle of an image with full bounds stored in init.
115 * @since 13127
116 */
117 public static class VisRect extends Rectangle {
118 private final Rectangle init;
119
120 /** set when this {@code VisRect} is updated by a mouse drag operation and
121 * unset on mouse release **/
122 public boolean isDragUpdate;
123
124 /**
125 * Constructs a new {@code VisRect}.
126 * @param x the specified X coordinate
127 * @param y the specified Y coordinate
128 * @param width the width of the rectangle
129 * @param height the height of the rectangle
130 */
131 public VisRect(int x, int y, int width, int height) {
132 super(x, y, width, height);
133 init = new Rectangle(this);
134 }
135
136 /**
137 * Constructs a new {@code VisRect}.
138 * @param x the specified X coordinate
139 * @param y the specified Y coordinate
140 * @param width the width of the rectangle
141 * @param height the height of the rectangle
142 * @param peer share full bounds with this peer {@code VisRect}
143 */
144 public VisRect(int x, int y, int width, int height, VisRect peer) {
145 super(x, y, width, height);
146 init = peer.init;
147 }
148
149 /**
150 * Constructs a new {@code VisRect} from another one.
151 * @param v rectangle to copy
152 */
153 public VisRect(VisRect v) {
154 super(v);
155 init = v.init;
156 }
157
158 /**
159 * Constructs a new empty {@code VisRect}.
160 */
161 public VisRect() {
162 this(0, 0, 0, 0);
163 }
164
165 public boolean isFullView() {
166 return init.equals(this);
167 }
168
169 public boolean isFullView1D() {
170 return (init.x == x && init.width == width)
171 || (init.y == y && init.height == height);
172 }
173
174 public void reset() {
175 setBounds(init);
176 }
177
178 public void checkRectPos() {
179 if (x < 0) {
180 x = 0;
181 }
182 if (y < 0) {
183 y = 0;
184 }
185 if (x + width > init.width) {
186 x = init.width - width;
187 }
188 if (y + height > init.height) {
189 y = init.height - height;
190 }
191 }
192
193 public void checkRectSize() {
194 if (width > init.width) {
195 width = init.width;
196 }
197 if (height > init.height) {
198 height = init.height;
199 }
200 }
201
202 public void checkPointInside(Point p) {
203 if (p.x < x) {
204 p.x = x;
205 }
206 if (p.x > x + width) {
207 p.x = x + width;
208 }
209 if (p.y < y) {
210 p.y = y;
211 }
212 if (p.y > y + height) {
213 p.y = y + height;
214 }
215 }
216 }
217
218 /** The thread that reads the images. */
219 private class LoadImageRunnable implements Runnable, ImageObserver {
220
221 private final ImageEntry entry;
222 private final File file;
223
224 LoadImageRunnable(ImageEntry entry) {
225 this.entry = entry;
226 this.file = entry.getFile();
227 }
228
229 @Override
230 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
231 if (((infoflags & ImageObserver.WIDTH) == ImageObserver.WIDTH) &&
232 ((infoflags & ImageObserver.HEIGHT) == ImageObserver.HEIGHT)) {
233 synchronized (entry) {
234 entry.setWidth(width);
235 entry.setHeight(height);
236 entry.notifyAll();
237 return false;
238 }
239 }
240 return true;
241 }
242
243 private boolean updateImageEntry(Image img) {
244 if (!(entry.getWidth() > 0 && entry.getHeight() > 0)) {
245 synchronized (entry) {
246 img.getWidth(this);
247 img.getHeight(this);
248
249 long now = System.currentTimeMillis();
250 while (!(entry.getWidth() > 0 && entry.getHeight() > 0)) {
251 try {
252 entry.wait(1000);
253 if (this.entry != ImageDisplay.this.entry)
254 return false;
255 if (System.currentTimeMillis() - now > 10000)
256 synchronized (ImageDisplay.this) {
257 errorLoading = true;
258 ImageDisplay.this.repaint();
259 return false;
260 }
261 } catch (InterruptedException e) {
262 Logging.trace(e);
263 Logging.warn("InterruptedException in {0} while getting properties of image {1}",
264 getClass().getSimpleName(), file.getPath());
265 Thread.currentThread().interrupt();
266 }
267 }
268 }
269 }
270 return true;
271 }
272
273 private boolean mayFitMemory(long amountWanted) {
274 return amountWanted < (
275 Runtime.getRuntime().maxMemory() -
276 Runtime.getRuntime().totalMemory() +
277 Runtime.getRuntime().freeMemory());
278 }
279
280 @Override
281 public void run() {
282 Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
283 if (!updateImageEntry(img))
284 return;
285
286 int width = entry.getWidth();
287 int height = entry.getHeight();
288
289 if (mayFitMemory(((long) width)*height*4*2)) {
290 Logging.info(tr("Loading {0}", file.getPath()));
291 tracker.addImage(img, 1);
292
293 // Wait for the end of loading
294 while (!tracker.checkID(1, true)) {
295 if (this.entry != ImageDisplay.this.entry) {
296 // The file has changed
297 tracker.removeImage(img);
298 return;
299 }
300 try {
301 Thread.sleep(5);
302 } catch (InterruptedException e) {
303 Logging.trace(e);
304 Logging.warn("InterruptedException in {0} while loading image {1}",
305 getClass().getSimpleName(), file.getPath());
306 Thread.currentThread().interrupt();
307 }
308 }
309 if (tracker.isErrorID(1)) {
310 Logging.warn("Abort loading of {0} since tracker errored with 1", file);
311 // the tracker catches OutOfMemory conditions
312 tracker.removeImage(img);
313 img = null;
314 } else {
315 tracker.removeImage(img);
316 }
317 } else {
318 Logging.warn("Abort loading of {0} since it might not fit into memory", file);
319 img = null;
320 }
321
322 synchronized (ImageDisplay.this) {
323 if (this.entry != ImageDisplay.this.entry) {
324 // The file has changed
325 return;
326 }
327
328 if (img != null) {
329 boolean switchedDim = false;
330 if (ExifReader.orientationNeedsCorrection(entry.getExifOrientation())) {
331 if (ExifReader.orientationSwitchesDimensions(entry.getExifOrientation())) {
332 width = img.getHeight(null);
333 height = img.getWidth(null);
334 switchedDim = true;
335 }
336 final BufferedImage rot = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
337 final AffineTransform xform = ExifReader.getRestoreOrientationTransform(
338 entry.getExifOrientation(),
339 img.getWidth(null),
340 img.getHeight(null));
341 final Graphics2D g = rot.createGraphics();
342 g.drawImage(img, xform, null);
343 g.dispose();
344 img = rot;
345 }
346
347 ImageDisplay.this.image = img;
348 visibleRect = new VisRect(0, 0, width, height);
349
350 Logging.debug("Loaded {0} with dimensions {1}x{2} memoryTaken={3}m exifOrientationSwitchedDimension={4}",
351 file.getPath(), width, height, width*height*4/1024/1024, switchedDim);
352 }
353
354 selectedRect = null;
355 errorLoading = (img == null);
356 }
357 ImageDisplay.this.repaint();
358 }
359 }
360
361 private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
362
363 private MouseEvent lastMouseEvent;
364 private Point mousePointInImg;
365
366 private boolean mouseIsDragging(MouseEvent e) {
367 return (dragButton == 1 && SwingUtilities.isLeftMouseButton(e)) ||
368 (dragButton == 2 && SwingUtilities.isMiddleMouseButton(e)) ||
369 (dragButton == 3 && SwingUtilities.isRightMouseButton(e));
370 }
371
372 private boolean mouseIsZoomSelecting(MouseEvent e) {
373 return (zoomButton == 1 && SwingUtilities.isLeftMouseButton(e)) ||
374 (zoomButton == 2 && SwingUtilities.isMiddleMouseButton(e)) ||
375 (zoomButton == 3 && SwingUtilities.isRightMouseButton(e));
376 }
377
378 private boolean isAtMaxZoom(Rectangle visibleRect) {
379 return (visibleRect.width == (int) (getSize().width / MAX_ZOOM.get()) ||
380 visibleRect.height == (int) (getSize().height / MAX_ZOOM.get()));
381 }
382
383 private void mouseWheelMovedImpl(int x, int y, int rotation, boolean refreshMousePointInImg) {
384 ImageEntry entry;
385 Image image;
386 VisRect visibleRect;
387
388 synchronized (ImageDisplay.this) {
389 entry = ImageDisplay.this.entry;
390 image = ImageDisplay.this.image;
391 visibleRect = ImageDisplay.this.visibleRect;
392 }
393
394 selectedRect = null;
395
396 if (image == null)
397 return;
398
399 // Calculate the mouse cursor position in image coordinates to center the zoom.
400 if (refreshMousePointInImg)
401 mousePointInImg = comp2imgCoord(visibleRect, x, y, getSize());
402
403 // Apply the zoom to the visible rectangle in image coordinates
404 if (rotation > 0) {
405 visibleRect.width = (int) (visibleRect.width * ZOOM_STEP.get());
406 visibleRect.height = (int) (visibleRect.height * ZOOM_STEP.get());
407 } else {
408 visibleRect.width = (int) (visibleRect.width / ZOOM_STEP.get());
409 visibleRect.height = (int) (visibleRect.height / ZOOM_STEP.get());
410 }
411
412 // Check that the zoom doesn't exceed MAX_ZOOM:1
413 if (visibleRect.width < getSize().width / MAX_ZOOM.get()) {
414 visibleRect.width = (int) (getSize().width / MAX_ZOOM.get());
415 }
416 if (visibleRect.height < getSize().height / MAX_ZOOM.get()) {
417 visibleRect.height = (int) (getSize().height / MAX_ZOOM.get());
418 }
419
420 // Set the same ratio for the visible rectangle and the display area
421 int hFact = visibleRect.height * getSize().width;
422 int wFact = visibleRect.width * getSize().height;
423 if (hFact > wFact) {
424 visibleRect.width = hFact / getSize().height;
425 } else {
426 visibleRect.height = wFact / getSize().width;
427 }
428
429 // The size of the visible rectangle is limited by the image size.
430 visibleRect.checkRectSize();
431
432 // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
433 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, getSize());
434 visibleRect.x = mousePointInImg.x + ((drawRect.x - x) * visibleRect.width) / drawRect.width;
435 visibleRect.y = mousePointInImg.y + ((drawRect.y - y) * visibleRect.height) / drawRect.height;
436
437 // The position is also limited by the image size
438 visibleRect.checkRectPos();
439
440 synchronized (ImageDisplay.this) {
441 if (ImageDisplay.this.entry == entry) {
442 ImageDisplay.this.visibleRect = visibleRect;
443 }
444 }
445 ImageDisplay.this.repaint();
446 }
447
448 /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
449 * at the same place */
450 @Override
451 public void mouseWheelMoved(MouseWheelEvent e) {
452 boolean refreshMousePointInImg = false;
453
454 // To avoid issues when the user tries to zoom in on the image borders, this
455 // point is not recalculated as long as e occurs at roughly the same position.
456 if (lastMouseEvent == null || mousePointInImg == null ||
457 ((lastMouseEvent.getX()-e.getX())*(lastMouseEvent.getX()-e.getX())
458 +(lastMouseEvent.getY()-e.getY())*(lastMouseEvent.getY()-e.getY()) > 4*4)) {
459 lastMouseEvent = e;
460 refreshMousePointInImg = true;
461 }
462
463 mouseWheelMovedImpl(e.getX(), e.getY(), e.getWheelRotation(), refreshMousePointInImg);
464 }
465
466 /** Center the display on the point that has been clicked */
467 @Override
468 public void mouseClicked(MouseEvent e) {
469 // Move the center to the clicked point.
470 ImageEntry entry;
471 Image image;
472 VisRect visibleRect;
473
474 synchronized (ImageDisplay.this) {
475 entry = ImageDisplay.this.entry;
476 image = ImageDisplay.this.image;
477 visibleRect = ImageDisplay.this.visibleRect;
478 }
479
480 if (image == null)
481 return;
482
483 if (ZOOM_ON_CLICK.get()) {
484 // click notions are less coherent than wheel, refresh mousePointInImg on each click
485 lastMouseEvent = null;
486
487 if (mouseIsZoomSelecting(e) && !isAtMaxZoom(visibleRect)) {
488 // zoom in if clicked with the zoom button
489 mouseWheelMovedImpl(e.getX(), e.getY(), -1, true);
490 return;
491 }
492 if (mouseIsDragging(e)) {
493 // zoom out if clicked with the drag button
494 mouseWheelMovedImpl(e.getX(), e.getY(), 1, true);
495 return;
496 }
497 }
498
499 // Calculate the translation to set the clicked point the center of the view.
500 Point click = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
501 Point center = getCenterImgCoord(visibleRect);
502
503 visibleRect.x += click.x - center.x;
504 visibleRect.y += click.y - center.y;
505
506 visibleRect.checkRectPos();
507
508 synchronized (ImageDisplay.this) {
509 if (ImageDisplay.this.entry == entry) {
510 ImageDisplay.this.visibleRect = visibleRect;
511 }
512 }
513 ImageDisplay.this.repaint();
514 }
515
516 /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of
517 * a picture part) */
518 @Override
519 public void mousePressed(MouseEvent e) {
520 Image image;
521 VisRect visibleRect;
522
523 synchronized (ImageDisplay.this) {
524 image = ImageDisplay.this.image;
525 visibleRect = ImageDisplay.this.visibleRect;
526 }
527
528 if (image == null)
529 return;
530
531 selectedRect = null;
532
533 if (mouseIsDragging(e) || mouseIsZoomSelecting(e))
534 mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
535 }
536
537 @Override
538 public void mouseDragged(MouseEvent e) {
539 if (!mouseIsDragging(e) && !mouseIsZoomSelecting(e))
540 return;
541
542 ImageEntry entry;
543 Image image;
544 VisRect visibleRect;
545
546 synchronized (ImageDisplay.this) {
547 entry = ImageDisplay.this.entry;
548 image = ImageDisplay.this.image;
549 visibleRect = ImageDisplay.this.visibleRect;
550 }
551
552 if (image == null)
553 return;
554
555 if (mouseIsDragging(e) && mousePointInImg != null) {
556 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
557 visibleRect.isDragUpdate = true;
558 visibleRect.x += mousePointInImg.x - p.x;
559 visibleRect.y += mousePointInImg.y - p.y;
560 visibleRect.checkRectPos();
561 synchronized (ImageDisplay.this) {
562 if (ImageDisplay.this.entry == entry) {
563 ImageDisplay.this.visibleRect = visibleRect;
564 }
565 }
566 ImageDisplay.this.repaint();
567 }
568
569 if (mouseIsZoomSelecting(e) && mousePointInImg != null) {
570 Point p = comp2imgCoord(visibleRect, e.getX(), e.getY(), getSize());
571 visibleRect.checkPointInside(p);
572 VisRect selectedRect = new VisRect(
573 p.x < mousePointInImg.x ? p.x : mousePointInImg.x,
574 p.y < mousePointInImg.y ? p.y : mousePointInImg.y,
575 p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
576 p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y,
577 visibleRect);
578 selectedRect.checkRectSize();
579 selectedRect.checkRectPos();
580 ImageDisplay.this.selectedRect = selectedRect;
581 ImageDisplay.this.repaint();
582 }
583
584 }
585
586 @Override
587 public void mouseReleased(MouseEvent e) {
588 ImageEntry entry;
589 Image image;
590 VisRect visibleRect;
591
592 synchronized (ImageDisplay.this) {
593 entry = ImageDisplay.this.entry;
594 image = ImageDisplay.this.image;
595 visibleRect = ImageDisplay.this.visibleRect;
596 }
597
598 if (image == null)
599 return;
600
601 if (mouseIsDragging(e)) {
602 visibleRect.isDragUpdate = false;
603 }
604
605 if (mouseIsZoomSelecting(e) && selectedRect != null) {
606 int oldWidth = selectedRect.width;
607 int oldHeight = selectedRect.height;
608
609 // Check that the zoom doesn't exceed MAX_ZOOM:1
610 if (selectedRect.width < getSize().width / MAX_ZOOM.get()) {
611 selectedRect.width = (int) (getSize().width / MAX_ZOOM.get());
612 }
613 if (selectedRect.height < getSize().height / MAX_ZOOM.get()) {
614 selectedRect.height = (int) (getSize().height / MAX_ZOOM.get());
615 }
616
617 // Set the same ratio for the visible rectangle and the display area
618 int hFact = selectedRect.height * getSize().width;
619 int wFact = selectedRect.width * getSize().height;
620 if (hFact > wFact) {
621 selectedRect.width = hFact / getSize().height;
622 } else {
623 selectedRect.height = wFact / getSize().width;
624 }
625
626 // Keep the center of the selection
627 if (selectedRect.width != oldWidth) {
628 selectedRect.x -= (selectedRect.width - oldWidth) / 2;
629 }
630 if (selectedRect.height != oldHeight) {
631 selectedRect.y -= (selectedRect.height - oldHeight) / 2;
632 }
633
634 selectedRect.checkRectSize();
635 selectedRect.checkRectPos();
636 }
637
638 synchronized (ImageDisplay.this) {
639 if (entry == ImageDisplay.this.entry) {
640 if (selectedRect == null) {
641 ImageDisplay.this.visibleRect = visibleRect;
642 } else {
643 ImageDisplay.this.visibleRect.setBounds(selectedRect);
644 selectedRect = null;
645 }
646 }
647 }
648 ImageDisplay.this.repaint();
649 }
650
651 @Override
652 public void mouseEntered(MouseEvent e) {
653 // Do nothing
654 }
655
656 @Override
657 public void mouseExited(MouseEvent e) {
658 // Do nothing
659 }
660
661 @Override
662 public void mouseMoved(MouseEvent e) {
663 // Do nothing
664 }
665 }
666
667 /**
668 * Constructs a new {@code ImageDisplay}.
669 */
670 public ImageDisplay() {
671 addMouseListener(imgMouseListener);
672 addMouseWheelListener(imgMouseListener);
673 addMouseMotionListener(imgMouseListener);
674 Config.getPref().addPreferenceChangeListener(this);
675 preferenceChanged(null);
676 }
677
678 @Override
679 public void destroy() {
680 removeMouseListener(imgMouseListener);
681 removeMouseWheelListener(imgMouseListener);
682 removeMouseMotionListener(imgMouseListener);
683 Config.getPref().removePreferenceChangeListener(this);
684 }
685
686 /**
687 * Sets a new source image to be displayed by this {@code ImageDisplay}.
688 * @param entry new source image
689 * @since 13220
690 */
691 public void setImage(ImageEntry entry) {
692 synchronized (this) {
693 this.entry = entry;
694 image = null;
695 errorLoading = false;
696 }
697 repaint();
698 if (entry != null) {
699 new Thread(new LoadImageRunnable(entry), LoadImageRunnable.class.getName()).start();
700 }
701 }
702
703 /**
704 * Set the message displayed when there is no image to display.
705 * By default it display a simple No image
706 * @param emptyText the string to display
707 * @since 15333
708 */
709 public void setEmptyText(String emptyText) {
710 this.emptyText = emptyText;
711 }
712
713 /**
714 * Sets the On-Screen-Display text.
715 * @param text text to display on top of the image
716 */
717 public void setOsdText(String text) {
718 if (!text.equals(this.osdText)) {
719 this.osdText = text;
720 repaint();
721 }
722 }
723
724 @Override
725 public void paintComponent(Graphics g) {
726 ImageEntry entry;
727 Image image;
728 VisRect visibleRect;
729 boolean errorLoading;
730
731 synchronized (this) {
732 image = this.image;
733 entry = this.entry;
734 visibleRect = this.visibleRect;
735 errorLoading = this.errorLoading;
736 }
737
738 if (g instanceof Graphics2D) {
739 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
740 }
741
742 Dimension size = getSize();
743 if (entry == null) {
744 g.setColor(Color.black);
745 if (emptyText == null) {
746 emptyText = tr("No image");
747 }
748 String noImageStr = emptyText;
749 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
750 g.drawString(noImageStr,
751 (int) ((size.width - noImageSize.getWidth()) / 2),
752 (int) ((size.height - noImageSize.getHeight()) / 2));
753 } else if (image == null) {
754 g.setColor(Color.black);
755 String loadingStr;
756 if (!errorLoading) {
757 loadingStr = tr("Loading {0}", entry.getFile().getName());
758 } else {
759 loadingStr = tr("Error on file {0}", entry.getFile().getName());
760 }
761 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
762 g.drawString(loadingStr,
763 (int) ((size.width - noImageSize.getWidth()) / 2),
764 (int) ((size.height - noImageSize.getHeight()) / 2));
765 } else {
766 Rectangle r = new Rectangle(visibleRect);
767 Rectangle target = calculateDrawImageRectangle(visibleRect, size);
768 double scale = target.width / (double) r.width; // pixel ratio is 1:1
769
770 if (selectedRect == null && !visibleRect.isDragUpdate &&
771 bilinLower < scale && scale < bilinUpper) {
772 try {
773 BufferedImage bi = ImageProvider.toBufferedImage(image, r);
774 if (bi != null) {
775 r.x = r.y = 0;
776
777 // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
778 // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
779 bi = ImageProvider.createScaledImage(bi, target.width, target.height,
780 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
781 r.width = target.width;
782 r.height = target.height;
783 image = bi;
784 }
785 } catch (OutOfMemoryError oom) {
786 Logging.trace(oom);
787 // fall-back to the non-bilinear scaler
788 r.x = visibleRect.x;
789 r.y = visibleRect.y;
790 }
791 } else {
792 // if target and r cause drawImage to scale image region to a tmp buffer exceeding
793 // its bounds, it will silently fail; crop with r first in such cases
794 // (might be impl. dependent, exhibited by openjdk 1.8.0_151)
795 if (scale*(r.x+r.width) > Short.MAX_VALUE || scale*(r.y+r.height) > Short.MAX_VALUE) {
796 image = ImageProvider.toBufferedImage(image, r);
797 r.x = r.y = 0;
798 }
799 }
800
801 g.drawImage(image,
802 target.x, target.y, target.x + target.width, target.y + target.height,
803 r.x, r.y, r.x + r.width, r.y + r.height, null);
804
805 if (selectedRect != null) {
806 Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size);
807 Point bottomRight = img2compCoord(visibleRect,
808 selectedRect.x + selectedRect.width,
809 selectedRect.y + selectedRect.height, size);
810 g.setColor(new Color(128, 128, 128, 180));
811 g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
812 g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
813 g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
814 g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
815 g.setColor(Color.black);
816 g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
817 }
818 if (errorLoading) {
819 String loadingStr = tr("Error on file {0}", entry.getFile().getName());
820 Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
821 g.drawString(loadingStr,
822 (int) ((size.width - noImageSize.getWidth()) / 2),
823 (int) ((size.height - noImageSize.getHeight()) / 2));
824 }
825 if (osdText != null) {
826 FontMetrics metrics = g.getFontMetrics(g.getFont());
827 int ascent = metrics.getAscent();
828 Color bkground = new Color(255, 255, 255, 128);
829 int lastPos = 0;
830 int pos = osdText.indexOf('\n');
831 int x = 3;
832 int y = 3;
833 String line;
834 while (pos > 0) {
835 line = osdText.substring(lastPos, pos);
836 Rectangle2D lineSize = metrics.getStringBounds(line, g);
837 g.setColor(bkground);
838 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
839 g.setColor(Color.black);
840 g.drawString(line, x, y + ascent);
841 y += (int) lineSize.getHeight();
842 lastPos = pos + 1;
843 pos = osdText.indexOf('\n', lastPos);
844 }
845
846 line = osdText.substring(lastPos);
847 Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g);
848 g.setColor(bkground);
849 g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
850 g.setColor(Color.black);
851 g.drawString(line, x, y + ascent);
852 }
853 }
854 }
855
856 static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) {
857 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
858 return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
859 drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
860 }
861
862 static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) {
863 Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
864 Point p = new Point(
865 ((xComp - drawRect.x) * visibleRect.width),
866 ((yComp - drawRect.y) * visibleRect.height));
867 p.x += (((p.x % drawRect.width) << 1) >= drawRect.width) ? drawRect.width : 0;
868 p.y += (((p.y % drawRect.height) << 1) >= drawRect.height) ? drawRect.height : 0;
869 p.x = visibleRect.x + p.x / drawRect.width;
870 p.y = visibleRect.y + p.y / drawRect.height;
871 return p;
872 }
873
874 static Point getCenterImgCoord(Rectangle visibleRect) {
875 return new Point(visibleRect.x + visibleRect.width / 2,
876 visibleRect.y + visibleRect.height / 2);
877 }
878
879 static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) {
880 return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height));
881 }
882
883 /**
884 * calculateDrawImageRectangle
885 *
886 * @param imgRect the part of the image that should be drawn (in image coordinates)
887 * @param compRect the part of the component where the image should be drawn (in component coordinates)
888 * @return the part of compRect with the same width/height ratio as the image
889 */
890 static VisRect calculateDrawImageRectangle(VisRect imgRect, Rectangle compRect) {
891 int x = 0;
892 int y = 0;
893 int w = compRect.width;
894 int h = compRect.height;
895
896 int wFact = w * imgRect.height;
897 int hFact = h * imgRect.width;
898 if (wFact != hFact) {
899 if (wFact > hFact) {
900 w = hFact / imgRect.height;
901 x = (compRect.width - w) / 2;
902 } else {
903 h = wFact / imgRect.width;
904 y = (compRect.height - h) / 2;
905 }
906 }
907
908 // overscan to prevent empty edges when zooming in to zoom scales > 2:1
909 if (w > imgRect.width && h > imgRect.height && !imgRect.isFullView1D() && wFact != hFact) {
910 if (wFact > hFact) {
911 w = compRect.width;
912 x = 0;
913 h = wFact / imgRect.width;
914 y = (compRect.height - h) / 2;
915 } else {
916 h = compRect.height;
917 y = 0;
918 w = hFact / imgRect.height;
919 x = (compRect.width - w) / 2;
920 }
921 }
922
923 return new VisRect(x + compRect.x, y + compRect.y, w, h, imgRect);
924 }
925
926 /**
927 * Make the current image either scale to fit inside this component,
928 * or show a portion of image (1:1), if the image size is larger than
929 * the component size.
930 */
931 public void zoomBestFitOrOne() {
932 ImageEntry entry;
933 Image image;
934 VisRect visibleRect;
935
936 synchronized (this) {
937 entry = this.entry;
938 image = this.image;
939 visibleRect = this.visibleRect;
940 }
941
942 if (image == null)
943 return;
944
945 if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
946 // The display is not at best fit. => Zoom to best fit
947 visibleRect.reset();
948 } else {
949 // The display is at best fit => zoom to 1:1
950 Point center = getCenterImgCoord(visibleRect);
951 visibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2,
952 getWidth(), getHeight());
953 visibleRect.checkRectSize();
954 visibleRect.checkRectPos();
955 }
956
957 synchronized (this) {
958 if (this.entry == entry) {
959 this.visibleRect = visibleRect;
960 }
961 }
962 repaint();
963 }
964}
Note: See TracBrowser for help on using the repository browser.