001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.layer.geoimage;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Dimension;
008import java.awt.FontMetrics;
009import java.awt.Graphics;
010import java.awt.Graphics2D;
011import java.awt.Image;
012import java.awt.MediaTracker;
013import java.awt.Point;
014import java.awt.Rectangle;
015import java.awt.RenderingHints;
016import java.awt.Toolkit;
017import java.awt.event.MouseEvent;
018import java.awt.event.MouseListener;
019import java.awt.event.MouseMotionListener;
020import java.awt.event.MouseWheelEvent;
021import java.awt.event.MouseWheelListener;
022import java.awt.geom.AffineTransform;
023import java.awt.geom.Rectangle2D;
024import java.awt.image.BufferedImage;
025import java.awt.image.ImageObserver;
026import java.io.File;
027
028import javax.swing.JComponent;
029import javax.swing.SwingUtilities;
030
031import org.openstreetmap.josm.data.preferences.BooleanProperty;
032import org.openstreetmap.josm.data.preferences.DoubleProperty;
033import org.openstreetmap.josm.spi.preferences.Config;
034import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
035import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
036import org.openstreetmap.josm.tools.Destroyable;
037import org.openstreetmap.josm.tools.ExifReader;
038import org.openstreetmap.josm.tools.ImageProvider;
039import org.openstreetmap.josm.tools.Logging;
040
041/**
042 * GUI component to display an image (photograph).
043 *
044 * Offers basic mouse interaction (zoom, drag) and on-screen text.
045 */
046public class ImageDisplay extends JComponent implements Destroyable, PreferenceChangedListener {
047
048    /** The file that is currently displayed */
049    private ImageEntry entry;
050
051    /** The image currently displayed */
052    private transient Image image;
053
054    /** The image currently displayed */
055    private boolean errorLoading;
056
057    /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
058     * each time the zoom is modified */
059    private VisRect visibleRect;
060
061    /** When a selection is done, the rectangle of the selection (in image coordinates) */
062    private VisRect selectedRect;
063
064    /** The tracker to load the images */
065    private final MediaTracker tracker = new MediaTracker(this);
066
067    private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener();
068
069    private String osdText;
070
071    private static final BooleanProperty AGPIFO_STYLE =
072        new BooleanProperty("geoimage.agpifo-style-drag-and-zoom", false);
073    private static int dragButton;
074    private static int zoomButton;
075
076    /** Alternative to mouse wheel zoom; esp. handy if no mouse wheel is present **/
077    private static final BooleanProperty ZOOM_ON_CLICK =
078        new BooleanProperty("geoimage.use-mouse-clicks-to-zoom", true);
079
080    /** Zoom factor when click or wheel zooming **/
081    private static final DoubleProperty ZOOM_STEP =
082        new DoubleProperty("geoimage.zoom-step-factor", 3 / 2.0);
083
084    /** Maximum zoom allowed **/
085    private static final DoubleProperty MAX_ZOOM =
086        new DoubleProperty("geoimage.maximum-zoom-scale", 2.0);
087
088    /** Use bilinear filtering **/
089    private static final BooleanProperty BILIN_DOWNSAMP =
090        new BooleanProperty("geoimage.bilinear-downsampling-progressive", true);
091    private static final BooleanProperty BILIN_UPSAMP =
092        new BooleanProperty("geoimage.bilinear-upsampling", false);
093    private static double bilinUpper;
094    private static double bilinLower;
095
096    @Override
097    public void preferenceChanged(PreferenceChangeEvent e) {
098        if (e == null ||
099            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) && mousePointInImg != null) {
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) && mousePointInImg != null) {
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        if (!text.equals(this.osdText)) {
706            this.osdText = text;
707            repaint();
708        }
709    }
710
711    @Override
712    public void paintComponent(Graphics g) {
713        ImageEntry entry;
714        Image image;
715        VisRect visibleRect;
716        boolean errorLoading;
717
718        synchronized (this) {
719            image = this.image;
720            entry = this.entry;
721            visibleRect = this.visibleRect;
722            errorLoading = this.errorLoading;
723        }
724
725        if (g instanceof Graphics2D) {
726            ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
727        }
728
729        Dimension size = getSize();
730        if (entry == null) {
731            g.setColor(Color.black);
732            String noImageStr = tr("No image");
733            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
734            g.drawString(noImageStr,
735                    (int) ((size.width - noImageSize.getWidth()) / 2),
736                    (int) ((size.height - noImageSize.getHeight()) / 2));
737        } else if (image == null) {
738            g.setColor(Color.black);
739            String loadingStr;
740            if (!errorLoading) {
741                loadingStr = tr("Loading {0}", entry.getFile().getName());
742            } else {
743                loadingStr = tr("Error on file {0}", entry.getFile().getName());
744            }
745            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
746            g.drawString(loadingStr,
747                    (int) ((size.width - noImageSize.getWidth()) / 2),
748                    (int) ((size.height - noImageSize.getHeight()) / 2));
749        } else {
750            Rectangle r = new Rectangle(visibleRect);
751            Rectangle target = calculateDrawImageRectangle(visibleRect, size);
752            double scale = target.width / (double) r.width; // pixel ratio is 1:1
753
754            if (selectedRect == null && !visibleRect.isDragUpdate &&
755                bilinLower < scale && scale < bilinUpper) {
756                try {
757                    BufferedImage bi = ImageProvider.toBufferedImage(image, r);
758                    if (bi != null) {
759                        r.x = r.y = 0;
760
761                        // See https://community.oracle.com/docs/DOC-983611 - The Perils of Image.getScaledInstance()
762                        // Pre-scale image when downscaling by more than two times to avoid aliasing from default algorithm
763                        bi = ImageProvider.createScaledImage(bi, target.width, target.height,
764                                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
765                        r.width = target.width;
766                        r.height = target.height;
767                        image = bi;
768                    }
769                } catch (OutOfMemoryError oom) {
770                    Logging.trace(oom);
771                    // fall-back to the non-bilinear scaler
772                    r.x = visibleRect.x;
773                    r.y = visibleRect.y;
774                }
775            } else {
776                // if target and r cause drawImage to scale image region to a tmp buffer exceeding
777                // its bounds, it will silently fail; crop with r first in such cases
778                // (might be impl. dependent, exhibited by openjdk 1.8.0_151)
779                if (scale*(r.x+r.width) > Short.MAX_VALUE || scale*(r.y+r.height) > Short.MAX_VALUE) {
780                    image = ImageProvider.toBufferedImage(image, r);
781                    r.x = r.y = 0;
782                }
783            }
784
785            g.drawImage(image,
786                    target.x, target.y, target.x + target.width, target.y + target.height,
787                    r.x, r.y, r.x + r.width, r.y + r.height, null);
788
789            if (selectedRect != null) {
790                Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y, size);
791                Point bottomRight = img2compCoord(visibleRect,
792                        selectedRect.x + selectedRect.width,
793                        selectedRect.y + selectedRect.height, size);
794                g.setColor(new Color(128, 128, 128, 180));
795                g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
796                g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
797                g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
798                g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
799                g.setColor(Color.black);
800                g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
801            }
802            if (errorLoading) {
803                String loadingStr = tr("Error on file {0}", entry.getFile().getName());
804                Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
805                g.drawString(loadingStr,
806                        (int) ((size.width - noImageSize.getWidth()) / 2),
807                        (int) ((size.height - noImageSize.getHeight()) / 2));
808            }
809            if (osdText != null) {
810                FontMetrics metrics = g.getFontMetrics(g.getFont());
811                int ascent = metrics.getAscent();
812                Color bkground = new Color(255, 255, 255, 128);
813                int lastPos = 0;
814                int pos = osdText.indexOf('\n');
815                int x = 3;
816                int y = 3;
817                String line;
818                while (pos > 0) {
819                    line = osdText.substring(lastPos, pos);
820                    Rectangle2D lineSize = metrics.getStringBounds(line, g);
821                    g.setColor(bkground);
822                    g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
823                    g.setColor(Color.black);
824                    g.drawString(line, x, y + ascent);
825                    y += (int) lineSize.getHeight();
826                    lastPos = pos + 1;
827                    pos = osdText.indexOf('\n', lastPos);
828                }
829
830                line = osdText.substring(lastPos);
831                Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g);
832                g.setColor(bkground);
833                g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
834                g.setColor(Color.black);
835                g.drawString(line, x, y + ascent);
836            }
837        }
838    }
839
840    static Point img2compCoord(VisRect visibleRect, int xImg, int yImg, Dimension compSize) {
841        Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
842        return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
843                drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
844    }
845
846    static Point comp2imgCoord(VisRect visibleRect, int xComp, int yComp, Dimension compSize) {
847        Rectangle drawRect = calculateDrawImageRectangle(visibleRect, compSize);
848        Point p = new Point(
849                        ((xComp - drawRect.x) * visibleRect.width),
850                        ((yComp - drawRect.y) * visibleRect.height));
851        p.x += (((p.x % drawRect.width) << 1) >= drawRect.width) ? drawRect.width : 0;
852        p.y += (((p.y % drawRect.height) << 1) >= drawRect.height) ? drawRect.height : 0;
853        p.x = visibleRect.x + p.x / drawRect.width;
854        p.y = visibleRect.y + p.y / drawRect.height;
855        return p;
856    }
857
858    static Point getCenterImgCoord(Rectangle visibleRect) {
859        return new Point(visibleRect.x + visibleRect.width / 2,
860                         visibleRect.y + visibleRect.height / 2);
861    }
862
863    static VisRect calculateDrawImageRectangle(VisRect visibleRect, Dimension compSize) {
864        return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, compSize.width, compSize.height));
865    }
866
867    /**
868     * calculateDrawImageRectangle
869     *
870     * @param imgRect the part of the image that should be drawn (in image coordinates)
871     * @param compRect the part of the component where the image should be drawn (in component coordinates)
872     * @return the part of compRect with the same width/height ratio as the image
873     */
874    static VisRect calculateDrawImageRectangle(VisRect imgRect, Rectangle compRect) {
875        int x = 0;
876        int y = 0;
877        int w = compRect.width;
878        int h = compRect.height;
879
880        int wFact = w * imgRect.height;
881        int hFact = h * imgRect.width;
882        if (wFact != hFact) {
883            if (wFact > hFact) {
884                w = hFact / imgRect.height;
885                x = (compRect.width - w) / 2;
886            } else {
887                h = wFact / imgRect.width;
888                y = (compRect.height - h) / 2;
889            }
890        }
891
892        // overscan to prevent empty edges when zooming in to zoom scales > 2:1
893        if (w > imgRect.width && h > imgRect.height && !imgRect.isFullView1D() && wFact != hFact) {
894            if (wFact > hFact) {
895                w = compRect.width;
896                x = 0;
897                h = wFact / imgRect.width;
898                y = (compRect.height - h) / 2;
899            } else {
900                h = compRect.height;
901                y = 0;
902                w = hFact / imgRect.height;
903                x = (compRect.width - w) / 2;
904            }
905        }
906
907        return new VisRect(x + compRect.x, y + compRect.y, w, h, imgRect);
908    }
909
910    /**
911     * Make the current image either scale to fit inside this component,
912     * or show a portion of image (1:1), if the image size is larger than
913     * the component size.
914     */
915    public void zoomBestFitOrOne() {
916        ImageEntry entry;
917        Image image;
918        VisRect visibleRect;
919
920        synchronized (this) {
921            entry = this.entry;
922            image = this.image;
923            visibleRect = this.visibleRect;
924        }
925
926        if (image == null)
927            return;
928
929        if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
930            // The display is not at best fit. => Zoom to best fit
931            visibleRect.reset();
932        } else {
933            // The display is at best fit => zoom to 1:1
934            Point center = getCenterImgCoord(visibleRect);
935            visibleRect.setBounds(center.x - getWidth() / 2, center.y - getHeight() / 2,
936                    getWidth(), getHeight());
937            visibleRect.checkRectSize();
938            visibleRect.checkRectPos();
939        }
940
941        synchronized (this) {
942            if (this.entry == entry) {
943                this.visibleRect = visibleRect;
944            }
945        }
946        repaint();
947    }
948}