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

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

sonar - squid:S1166 - Exception handlers should preserve the original exceptions

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