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