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