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

Last change on this file since 13220 was 13220, checked in by Don-vip, 7 years ago

see #15574:

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