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