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

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

fix #15625, see #15574 - geo image loading: do ram constraint checking; add some javadoc; do not use bilinear scaling when image is dragged (patch by cmuelle8, modified)

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