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.Toolkit;
016import java.awt.event.MouseEvent;
017import java.awt.event.MouseListener;
018import java.awt.event.MouseMotionListener;
019import java.awt.event.MouseWheelEvent;
020import java.awt.event.MouseWheelListener;
021import java.awt.geom.AffineTransform;
022import java.awt.geom.Rectangle2D;
023import java.awt.image.BufferedImage;
024import java.io.File;
025
026import javax.swing.JComponent;
027
028import org.openstreetmap.josm.Main;
029import org.openstreetmap.josm.tools.ExifReader;
030
031public class ImageDisplay extends JComponent {
032
033    /** The file that is currently displayed */
034    private File file;
035
036    /** The image currently displayed */
037    private transient Image image;
038
039    /** The image currently displayed */
040    private boolean errorLoading;
041
042    /** The rectangle (in image coordinates) of the image that is visible. This rectangle is calculated
043     * each time the zoom is modified */
044    private Rectangle visibleRect;
045
046    /** When a selection is done, the rectangle of the selection (in image coordinates) */
047    private Rectangle selectedRect;
048
049    /** The tracker to load the images */
050    private final MediaTracker tracker = new MediaTracker(this);
051
052    private String osdText;
053
054    private static final int DRAG_BUTTON = Main.pref.getBoolean("geoimage.agpifo-style-drag-and-zoom", false) ? 1 : 3;
055    private static final int ZOOM_BUTTON = DRAG_BUTTON == 1 ? 3 : 1;
056
057    /** The thread that reads the images. */
058    private class LoadImageRunnable implements Runnable {
059
060        private final File file;
061        private final int orientation;
062
063        LoadImageRunnable(File file, Integer orientation) {
064            this.file = file;
065            this.orientation = orientation == null ? -1 : orientation;
066        }
067
068        @Override
069        public void run() {
070            Image img = Toolkit.getDefaultToolkit().createImage(file.getPath());
071            tracker.addImage(img, 1);
072
073            // Wait for the end of loading
074            while (!tracker.checkID(1, true)) {
075                if (this.file != ImageDisplay.this.file) {
076                    // The file has changed
077                    tracker.removeImage(img);
078                    return;
079                }
080                try {
081                    Thread.sleep(5);
082                } catch (InterruptedException e) {
083                    Main.warn("InterruptedException in "+getClass().getSimpleName()+" while loading image "+file.getPath());
084                }
085            }
086
087            boolean error = tracker.isErrorID(1);
088            if (img.getWidth(null) < 0 || img.getHeight(null) < 0) {
089                error = true;
090            }
091
092            synchronized (ImageDisplay.this) {
093                if (this.file != ImageDisplay.this.file) {
094                    // The file has changed
095                    tracker.removeImage(img);
096                    return;
097                }
098
099                if (!error) {
100                    ImageDisplay.this.image = img;
101                    visibleRect = new Rectangle(0, 0, img.getWidth(null), img.getHeight(null));
102
103                    final int w = (int) visibleRect.getWidth();
104                    final int h = (int) visibleRect.getHeight();
105
106                    if (ExifReader.orientationNeedsCorrection(orientation)) {
107                        final int hh, ww;
108                        if (ExifReader.orientationSwitchesDimensions(orientation)) {
109                            ww = h;
110                            hh = w;
111                        } else {
112                            ww = w;
113                            hh = h;
114                        }
115                        final BufferedImage rot = new BufferedImage(ww, hh, BufferedImage.TYPE_INT_RGB);
116                        final AffineTransform xform = ExifReader.getRestoreOrientationTransform(orientation, w, h);
117                        final Graphics2D g = rot.createGraphics();
118                        g.drawImage(image, xform, null);
119                        g.dispose();
120
121                        visibleRect.setSize(ww, hh);
122                        image.flush();
123                        ImageDisplay.this.image = rot;
124                    }
125                }
126
127                selectedRect = null;
128                errorLoading = error;
129            }
130            tracker.removeImage(img);
131            ImageDisplay.this.repaint();
132        }
133    }
134
135    private class ImgDisplayMouseListener implements MouseListener, MouseWheelListener, MouseMotionListener {
136
137        private boolean mouseIsDragging;
138        private long lastTimeForMousePoint;
139        private Point mousePointInImg;
140
141        /** Zoom in and out, trying to preserve the point of the image that was under the mouse cursor
142         * at the same place */
143        @Override
144        public void mouseWheelMoved(MouseWheelEvent e) {
145            File file;
146            Image image;
147            Rectangle visibleRect;
148
149            synchronized (ImageDisplay.this) {
150                file = ImageDisplay.this.file;
151                image = ImageDisplay.this.image;
152                visibleRect = ImageDisplay.this.visibleRect;
153            }
154
155            mouseIsDragging = false;
156            selectedRect = null;
157
158            if (image == null)
159                return;
160
161            // Calculate the mouse cursor position in image coordinates, so that we can center the zoom
162            // on that mouse position.
163            // To avoid issues when the user tries to zoom in on the image borders, this point is not calculated
164            // again if there was less than 1.5seconds since the last event.
165            if (e.getWhen() - lastTimeForMousePoint > 1500 || mousePointInImg == null) {
166                lastTimeForMousePoint = e.getWhen();
167                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
168            }
169
170            // Applicate the zoom to the visible rectangle in image coordinates
171            if (e.getWheelRotation() > 0) {
172                visibleRect.width = visibleRect.width * 3 / 2;
173                visibleRect.height = visibleRect.height * 3 / 2;
174            } else {
175                visibleRect.width = visibleRect.width * 2 / 3;
176                visibleRect.height = visibleRect.height * 2 / 3;
177            }
178
179            // Check that the zoom doesn't exceed 2:1
180            if (visibleRect.width < getSize().width / 2) {
181                visibleRect.width = getSize().width / 2;
182            }
183            if (visibleRect.height < getSize().height / 2) {
184                visibleRect.height = getSize().height / 2;
185            }
186
187            // Set the same ratio for the visible rectangle and the display area
188            int hFact = visibleRect.height * getSize().width;
189            int wFact = visibleRect.width * getSize().height;
190            if (hFact > wFact) {
191                visibleRect.width = hFact / getSize().height;
192            } else {
193                visibleRect.height = wFact / getSize().width;
194            }
195
196            // The size of the visible rectangle is limited by the image size.
197            checkVisibleRectSize(image, visibleRect);
198
199            // Set the position of the visible rectangle, so that the mouse cursor doesn't move on the image.
200            Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
201            visibleRect.x = mousePointInImg.x + ((drawRect.x - e.getX()) * visibleRect.width) / drawRect.width;
202            visibleRect.y = mousePointInImg.y + ((drawRect.y - e.getY()) * visibleRect.height) / drawRect.height;
203
204            // The position is also limited by the image size
205            checkVisibleRectPos(image, visibleRect);
206
207            synchronized (ImageDisplay.this) {
208                if (ImageDisplay.this.file == file) {
209                    ImageDisplay.this.visibleRect = visibleRect;
210                }
211            }
212            ImageDisplay.this.repaint();
213        }
214
215        /** Center the display on the point that has been clicked */
216        @Override
217        public void mouseClicked(MouseEvent e) {
218            // Move the center to the clicked point.
219            File file;
220            Image image;
221            Rectangle visibleRect;
222
223            synchronized (ImageDisplay.this) {
224                file = ImageDisplay.this.file;
225                image = ImageDisplay.this.image;
226                visibleRect = ImageDisplay.this.visibleRect;
227            }
228
229            if (image == null)
230                return;
231
232            if (e.getButton() != DRAG_BUTTON)
233                return;
234
235            // Calculate the translation to set the clicked point the center of the view.
236            Point click = comp2imgCoord(visibleRect, e.getX(), e.getY());
237            Point center = getCenterImgCoord(visibleRect);
238
239            visibleRect.x += click.x - center.x;
240            visibleRect.y += click.y - center.y;
241
242            checkVisibleRectPos(image, visibleRect);
243
244            synchronized (ImageDisplay.this) {
245                if (ImageDisplay.this.file == file) {
246                    ImageDisplay.this.visibleRect = visibleRect;
247                }
248            }
249            ImageDisplay.this.repaint();
250        }
251
252        /** Initialize the dragging, either with button 1 (simple dragging) or button 3 (selection of
253         * a picture part) */
254        @Override
255        public void mousePressed(MouseEvent e) {
256            if (image == null) {
257                mouseIsDragging = false;
258                selectedRect = null;
259                return;
260            }
261
262            Image image;
263            Rectangle visibleRect;
264
265            synchronized (ImageDisplay.this) {
266                image = ImageDisplay.this.image;
267                visibleRect = ImageDisplay.this.visibleRect;
268            }
269
270            if (image == null)
271                return;
272
273            if (e.getButton() == DRAG_BUTTON) {
274                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
275                mouseIsDragging = true;
276                selectedRect = null;
277            } else if (e.getButton() == ZOOM_BUTTON) {
278                mousePointInImg = comp2imgCoord(visibleRect, e.getX(), e.getY());
279                checkPointInVisibleRect(mousePointInImg, visibleRect);
280                mouseIsDragging = false;
281                selectedRect = new Rectangle(mousePointInImg.x, mousePointInImg.y, 0, 0);
282                ImageDisplay.this.repaint();
283            } else {
284                mouseIsDragging = false;
285                selectedRect = null;
286            }
287        }
288
289        @Override
290        public void mouseDragged(MouseEvent e) {
291            if (!mouseIsDragging && selectedRect == null)
292                return;
293
294            File file;
295            Image image;
296            Rectangle visibleRect;
297
298            synchronized (ImageDisplay.this) {
299                file = ImageDisplay.this.file;
300                image = ImageDisplay.this.image;
301                visibleRect = ImageDisplay.this.visibleRect;
302            }
303
304            if (image == null) {
305                mouseIsDragging = false;
306                selectedRect = null;
307                return;
308            }
309
310            if (mouseIsDragging) {
311                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
312                visibleRect.x += mousePointInImg.x - p.x;
313                visibleRect.y += mousePointInImg.y - p.y;
314                checkVisibleRectPos(image, visibleRect);
315                synchronized (ImageDisplay.this) {
316                    if (ImageDisplay.this.file == file) {
317                        ImageDisplay.this.visibleRect = visibleRect;
318                    }
319                }
320                ImageDisplay.this.repaint();
321
322            } else if (selectedRect != null) {
323                Point p = comp2imgCoord(visibleRect, e.getX(), e.getY());
324                checkPointInVisibleRect(p, visibleRect);
325                Rectangle rect = new Rectangle(
326                        p.x < mousePointInImg.x ? p.x : mousePointInImg.x,
327                        p.y < mousePointInImg.y ? p.y : mousePointInImg.y,
328                        p.x < mousePointInImg.x ? mousePointInImg.x - p.x : p.x - mousePointInImg.x,
329                        p.y < mousePointInImg.y ? mousePointInImg.y - p.y : p.y - mousePointInImg.y);
330                checkVisibleRectSize(image, rect);
331                checkVisibleRectPos(image, rect);
332                ImageDisplay.this.selectedRect = rect;
333                ImageDisplay.this.repaint();
334            }
335
336        }
337
338        @Override
339        public void mouseReleased(MouseEvent e) {
340            if (!mouseIsDragging && selectedRect == null)
341                return;
342
343            File file;
344            Image image;
345
346            synchronized (ImageDisplay.this) {
347                file = ImageDisplay.this.file;
348                image = ImageDisplay.this.image;
349            }
350
351            if (image == null) {
352                mouseIsDragging = false;
353                selectedRect = null;
354                return;
355            }
356
357            if (mouseIsDragging) {
358                mouseIsDragging = false;
359
360            } else if (selectedRect != null) {
361                int oldWidth = selectedRect.width;
362                int oldHeight = selectedRect.height;
363
364                // Check that the zoom doesn't exceed 2:1
365                if (selectedRect.width < getSize().width / 2) {
366                    selectedRect.width = getSize().width / 2;
367                }
368                if (selectedRect.height < getSize().height / 2) {
369                    selectedRect.height = getSize().height / 2;
370                }
371
372                // Set the same ratio for the visible rectangle and the display area
373                int hFact = selectedRect.height * getSize().width;
374                int wFact = selectedRect.width * getSize().height;
375                if (hFact > wFact) {
376                    selectedRect.width = hFact / getSize().height;
377                } else {
378                    selectedRect.height = wFact / getSize().width;
379                }
380
381                // Keep the center of the selection
382                if (selectedRect.width != oldWidth) {
383                    selectedRect.x -= (selectedRect.width - oldWidth) / 2;
384                }
385                if (selectedRect.height != oldHeight) {
386                    selectedRect.y -= (selectedRect.height - oldHeight) / 2;
387                }
388
389                checkVisibleRectSize(image, selectedRect);
390                checkVisibleRectPos(image, selectedRect);
391
392                synchronized (ImageDisplay.this) {
393                    if (file == ImageDisplay.this.file) {
394                        ImageDisplay.this.visibleRect = selectedRect;
395                    }
396                }
397                selectedRect = null;
398                ImageDisplay.this.repaint();
399            }
400        }
401
402        @Override
403        public void mouseEntered(MouseEvent e) {
404            // Do nothing
405        }
406
407        @Override
408        public void mouseExited(MouseEvent e) {
409            // Do nothing
410        }
411
412        @Override
413        public void mouseMoved(MouseEvent e) {
414            // Do nothing
415        }
416
417        private void checkPointInVisibleRect(Point p, Rectangle visibleRect) {
418            if (p.x < visibleRect.x) {
419                p.x = visibleRect.x;
420            }
421            if (p.x > visibleRect.x + visibleRect.width) {
422                p.x = visibleRect.x + visibleRect.width;
423            }
424            if (p.y < visibleRect.y) {
425                p.y = visibleRect.y;
426            }
427            if (p.y > visibleRect.y + visibleRect.height) {
428                p.y = visibleRect.y + visibleRect.height;
429            }
430        }
431    }
432
433    public ImageDisplay() {
434        ImgDisplayMouseListener mouseListener = new ImgDisplayMouseListener();
435        addMouseListener(mouseListener);
436        addMouseWheelListener(mouseListener);
437        addMouseMotionListener(mouseListener);
438    }
439
440    public void setImage(File file, Integer orientation) {
441        synchronized (this) {
442            this.file = file;
443            image = null;
444            selectedRect = null;
445            errorLoading = false;
446        }
447        repaint();
448        if (file != null) {
449            new Thread(new LoadImageRunnable(file, orientation), LoadImageRunnable.class.getName()).start();
450        }
451    }
452
453    public void setOsdText(String text) {
454        this.osdText = text;
455        repaint();
456    }
457
458    @Override
459    public void paintComponent(Graphics g) {
460        Image image;
461        File file;
462        Rectangle visibleRect;
463        boolean errorLoading;
464
465        synchronized (this) {
466            image = this.image;
467            file = this.file;
468            visibleRect = this.visibleRect;
469            errorLoading = this.errorLoading;
470        }
471
472        if (file == null) {
473            g.setColor(Color.black);
474            String noImageStr = tr("No image");
475            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
476            Dimension size = getSize();
477            g.drawString(noImageStr,
478                    (int) ((size.width - noImageSize.getWidth()) / 2),
479                    (int) ((size.height - noImageSize.getHeight()) / 2));
480        } else if (image == null) {
481            g.setColor(Color.black);
482            String loadingStr;
483            if (!errorLoading) {
484                loadingStr = tr("Loading {0}", file.getName());
485            } else {
486                loadingStr = tr("Error on file {0}", file.getName());
487            }
488            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
489            Dimension size = getSize();
490            g.drawString(loadingStr,
491                    (int) ((size.width - noImageSize.getWidth()) / 2),
492                    (int) ((size.height - noImageSize.getHeight()) / 2));
493        } else {
494            Rectangle target = calculateDrawImageRectangle(visibleRect);
495            g.drawImage(image,
496                    target.x, target.y, target.x + target.width, target.y + target.height,
497                    visibleRect.x, visibleRect.y, visibleRect.x + visibleRect.width, visibleRect.y + visibleRect.height,
498                    null);
499            if (selectedRect != null) {
500                Point topLeft = img2compCoord(visibleRect, selectedRect.x, selectedRect.y);
501                Point bottomRight = img2compCoord(visibleRect,
502                        selectedRect.x + selectedRect.width,
503                        selectedRect.y + selectedRect.height);
504                g.setColor(new Color(128, 128, 128, 180));
505                g.fillRect(target.x, target.y, target.width, topLeft.y - target.y);
506                g.fillRect(target.x, target.y, topLeft.x - target.x, target.height);
507                g.fillRect(bottomRight.x, target.y, target.x + target.width - bottomRight.x, target.height);
508                g.fillRect(target.x, bottomRight.y, target.width, target.y + target.height - bottomRight.y);
509                g.setColor(Color.black);
510                g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
511            }
512            if (errorLoading) {
513                String loadingStr = tr("Error on file {0}", file.getName());
514                Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(loadingStr, g);
515                Dimension size = getSize();
516                g.drawString(loadingStr,
517                        (int) ((size.width - noImageSize.getWidth()) / 2),
518                        (int) ((size.height - noImageSize.getHeight()) / 2));
519            }
520            if (osdText != null) {
521                FontMetrics metrics = g.getFontMetrics(g.getFont());
522                int ascent = metrics.getAscent();
523                Color bkground = new Color(255, 255, 255, 128);
524                int lastPos = 0;
525                int pos = osdText.indexOf('\n');
526                int x = 3;
527                int y = 3;
528                String line;
529                while (pos > 0) {
530                    line = osdText.substring(lastPos, pos);
531                    Rectangle2D lineSize = metrics.getStringBounds(line, g);
532                    g.setColor(bkground);
533                    g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
534                    g.setColor(Color.black);
535                    g.drawString(line, x, y + ascent);
536                    y += (int) lineSize.getHeight();
537                    lastPos = pos + 1;
538                    pos = osdText.indexOf('\n', lastPos);
539                }
540
541                line = osdText.substring(lastPos);
542                Rectangle2D lineSize = g.getFontMetrics(g.getFont()).getStringBounds(line, g);
543                g.setColor(bkground);
544                g.fillRect(x, y, (int) lineSize.getWidth(), (int) lineSize.getHeight());
545                g.setColor(Color.black);
546                g.drawString(line, x, y + ascent);
547            }
548        }
549    }
550
551    private Point img2compCoord(Rectangle visibleRect, int xImg, int yImg) {
552        Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
553        return new Point(drawRect.x + ((xImg - visibleRect.x) * drawRect.width) / visibleRect.width,
554                drawRect.y + ((yImg - visibleRect.y) * drawRect.height) / visibleRect.height);
555    }
556
557    private Point comp2imgCoord(Rectangle visibleRect, int xComp, int yComp) {
558        Rectangle drawRect = calculateDrawImageRectangle(visibleRect);
559        return new Point(visibleRect.x + ((xComp - drawRect.x) * visibleRect.width) / drawRect.width,
560                visibleRect.y + ((yComp - drawRect.y) * visibleRect.height) / drawRect.height);
561    }
562
563    private static Point getCenterImgCoord(Rectangle visibleRect) {
564        return new Point(visibleRect.x + visibleRect.width / 2,
565                visibleRect.y + visibleRect.height / 2);
566    }
567
568    private Rectangle calculateDrawImageRectangle(Rectangle visibleRect) {
569        return calculateDrawImageRectangle(visibleRect, new Rectangle(0, 0, getSize().width, getSize().height));
570    }
571
572    /**
573     * calculateDrawImageRectangle
574     *
575     * @param imgRect the part of the image that should be drawn (in image coordinates)
576     * @param compRect the part of the component where the image should be drawn (in component coordinates)
577     * @return the part of compRect with the same width/height ratio as the image
578     */
579    static Rectangle calculateDrawImageRectangle(Rectangle imgRect, Rectangle compRect) {
580        int x, y, w, h;
581        x = 0;
582        y = 0;
583        w = compRect.width;
584        h = compRect.height;
585
586        int wFact = w * imgRect.height;
587        int hFact = h * imgRect.width;
588        if (wFact != hFact) {
589            if (wFact > hFact) {
590                w = hFact / imgRect.height;
591                x = (compRect.width - w) / 2;
592            } else {
593                h = wFact / imgRect.width;
594                y = (compRect.height - h) / 2;
595            }
596        }
597        return new Rectangle(x + compRect.x, y + compRect.y, w, h);
598    }
599
600    public void zoomBestFitOrOne() {
601        File file;
602        Image image;
603        Rectangle visibleRect;
604
605        synchronized (this) {
606            file = this.file;
607            image = this.image;
608            visibleRect = this.visibleRect;
609        }
610
611        if (image == null)
612            return;
613
614        if (visibleRect.width != image.getWidth(null) || visibleRect.height != image.getHeight(null)) {
615            // The display is not at best fit. => Zoom to best fit
616            visibleRect = new Rectangle(0, 0, image.getWidth(null), image.getHeight(null));
617
618        } else {
619            // The display is at best fit => zoom to 1:1
620            Point center = getCenterImgCoord(visibleRect);
621            visibleRect = new Rectangle(center.x - getWidth() / 2, center.y - getHeight() / 2,
622                    getWidth(), getHeight());
623            checkVisibleRectPos(image, visibleRect);
624        }
625
626        synchronized (this) {
627            if (file == this.file) {
628                this.visibleRect = visibleRect;
629            }
630        }
631        repaint();
632    }
633
634    private static void checkVisibleRectPos(Image image, Rectangle visibleRect) {
635        if (visibleRect.x < 0) {
636            visibleRect.x = 0;
637        }
638        if (visibleRect.y < 0) {
639            visibleRect.y = 0;
640        }
641        if (visibleRect.x + visibleRect.width > image.getWidth(null)) {
642            visibleRect.x = image.getWidth(null) - visibleRect.width;
643        }
644        if (visibleRect.y + visibleRect.height > image.getHeight(null)) {
645            visibleRect.y = image.getHeight(null) - visibleRect.height;
646        }
647    }
648
649    private static void checkVisibleRectSize(Image image, Rectangle visibleRect) {
650        if (visibleRect.width > image.getWidth(null)) {
651            visibleRect.width = image.getWidth(null);
652        }
653        if (visibleRect.height > image.getHeight(null)) {
654            visibleRect.height = image.getHeight(null);
655        }
656    }
657}