001    // License: GPL. See LICENSE file for details.
002    // Copyright 2007 by Christian Gallioz (aka khris78)
003    // Parts of code from Geotagged plugin (by Rob Neild)
004    // and the core JOSM source code (by Immanuel Scholz and others)
005    package org.openstreetmap.josm.gui.layer.geoimage;
006    
007    import static org.openstreetmap.josm.tools.I18n.tr;
008    import static org.openstreetmap.josm.tools.I18n.trn;
009    
010    import java.awt.AlphaComposite;
011    import java.awt.Color;
012    import java.awt.Composite;
013    import java.awt.Dimension;
014    import java.awt.Graphics2D;
015    import java.awt.Image;
016    import java.awt.Point;
017    import java.awt.Rectangle;
018    import java.awt.event.MouseAdapter;
019    import java.awt.event.MouseEvent;
020    import java.awt.image.BufferedImage;
021    import java.beans.PropertyChangeEvent;
022    import java.beans.PropertyChangeListener;
023    import java.io.File;
024    import java.io.IOException;
025    import java.text.ParseException;
026    import java.util.ArrayList;
027    import java.util.Arrays;
028    import java.util.Collection;
029    import java.util.Collections;
030    import java.util.HashSet;
031    import java.util.LinkedHashSet;
032    import java.util.LinkedList;
033    import java.util.List;
034    
035    import javax.swing.Action;
036    import javax.swing.Icon;
037    import javax.swing.JLabel;
038    import javax.swing.JOptionPane;
039    import javax.swing.SwingConstants;
040    
041    import org.openstreetmap.josm.Main;
042    import org.openstreetmap.josm.actions.RenameLayerAction;
043    import org.openstreetmap.josm.actions.mapmode.MapMode;
044    import org.openstreetmap.josm.actions.mapmode.SelectAction;
045    import org.openstreetmap.josm.data.Bounds;
046    import org.openstreetmap.josm.data.coor.LatLon;
047    import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
048    import org.openstreetmap.josm.gui.ExtendedDialog;
049    import org.openstreetmap.josm.gui.MapFrame;
050    import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
051    import org.openstreetmap.josm.gui.MapView;
052    import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
053    import org.openstreetmap.josm.gui.NavigatableComponent;
054    import org.openstreetmap.josm.gui.PleaseWaitRunnable;
055    import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
056    import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
057    import org.openstreetmap.josm.gui.layer.GpxLayer;
058    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToMarkerLayer;
059    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
060    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
061    import org.openstreetmap.josm.gui.layer.Layer;
062    import org.openstreetmap.josm.tools.ExifReader;
063    import org.openstreetmap.josm.tools.ImageProvider;
064    
065    import com.drew.imaging.jpeg.JpegMetadataReader;
066    import com.drew.lang.CompoundException;
067    import com.drew.lang.Rational;
068    import com.drew.metadata.Directory;
069    import com.drew.metadata.Metadata;
070    import com.drew.metadata.MetadataException;
071    import com.drew.metadata.exif.ExifDirectory;
072    import com.drew.metadata.exif.GpsDirectory;
073    
074    public class GeoImageLayer extends Layer implements PropertyChangeListener, JumpToMarkerLayer {
075    
076        List<ImageEntry> data;
077        GpxLayer gpxLayer;
078    
079        private Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
080        private Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
081    
082        private int currentPhoto = -1;
083    
084        boolean useThumbs = false;
085        ThumbsLoader thumbsloader;
086        boolean thumbsLoaded = false;
087        private BufferedImage offscreenBuffer;
088        boolean updateOffscreenBuffer = true;
089    
090        /** Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing.
091         * In facts, this object is instantiated with a list of files. These files may be JPEG files or
092         * directories. In case of directories, they are scanned to find all the images they contain.
093         * Then all the images that have be found are loaded as ImageEntry instances.
094         */
095        private static final class Loader extends PleaseWaitRunnable {
096    
097            private boolean canceled = false;
098            private GeoImageLayer layer;
099            private Collection<File> selection;
100            private HashSet<String> loadedDirectories = new HashSet<String>();
101            private LinkedHashSet<String> errorMessages;
102            private GpxLayer gpxLayer;
103    
104            protected void rememberError(String message) {
105                this.errorMessages.add(message);
106            }
107    
108            public Loader(Collection<File> selection, GpxLayer gpxLayer) {
109                super(tr("Extracting GPS locations from EXIF"));
110                this.selection = selection;
111                this.gpxLayer = gpxLayer;
112                errorMessages = new LinkedHashSet<String>();
113            }
114    
115            @Override protected void realRun() throws IOException {
116    
117                progressMonitor.subTask(tr("Starting directory scan"));
118                Collection<File> files = new ArrayList<File>();
119                try {
120                    addRecursiveFiles(files, selection);
121                } catch(NullPointerException npe) {
122                    rememberError(tr("One of the selected files was null"));
123                }
124    
125                if (canceled)
126                    return;
127                progressMonitor.subTask(tr("Read photos..."));
128                progressMonitor.setTicksCount(files.size());
129    
130                progressMonitor.subTask(tr("Read photos..."));
131                progressMonitor.setTicksCount(files.size());
132    
133                // read the image files
134                List<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
135    
136                for (File f : files) {
137    
138                    if (canceled) {
139                        break;
140                    }
141    
142                    progressMonitor.subTask(tr("Reading {0}...", f.getName()));
143                    progressMonitor.worked(1);
144    
145                    ImageEntry e = new ImageEntry();
146    
147                    // Changed to silently cope with no time info in exif. One case
148                    // of person having time that couldn't be parsed, but valid GPS info
149    
150                    try {
151                        e.setExifTime(ExifReader.readTime(f));
152                    } catch (ParseException e1) {
153                        e.setExifTime(null);
154                    }
155                    e.setFile(f);
156                    extractExif(e);
157                    data.add(e);
158                }
159                layer = new GeoImageLayer(data, gpxLayer);
160                files.clear();
161            }
162    
163            private void addRecursiveFiles(Collection<File> files, Collection<File> sel) {
164                boolean nullFile = false;
165    
166                for (File f : sel) {
167    
168                    if(canceled) {
169                        break;
170                    }
171    
172                    if (f == null) {
173                        nullFile = true;
174    
175                    } else if (f.isDirectory()) {
176                        String canonical = null;
177                        try {
178                            canonical = f.getCanonicalPath();
179                        } catch (IOException e) {
180                            e.printStackTrace();
181                            rememberError(tr("Unable to get canonical path for directory {0}\n",
182                                    f.getAbsolutePath()));
183                        }
184    
185                        if (canonical == null || loadedDirectories.contains(canonical)) {
186                            continue;
187                        } else {
188                            loadedDirectories.add(canonical);
189                        }
190    
191                        Collection<File> children = Arrays.asList(f.listFiles(JpegFileFilter.getInstance()));
192                        if (children != null) {
193                            progressMonitor.subTask(tr("Scanning directory {0}", f.getPath()));
194                            try {
195                                addRecursiveFiles(files, children);
196                            } catch(NullPointerException npe) {
197                                npe.printStackTrace();
198                                rememberError(tr("Found null file in directory {0}\n", f.getPath()));
199                            }
200                        } else {
201                            rememberError(tr("Error while getting files from directory {0}\n", f.getPath()));
202                        }
203    
204                    } else {
205                        files.add(f);
206                    }
207                }
208    
209                if (nullFile)
210                    throw new NullPointerException();
211            }
212    
213            protected String formatErrorMessages() {
214                StringBuilder sb = new StringBuilder();
215                sb.append("<html>");
216                if (errorMessages.size() == 1) {
217                    sb.append(errorMessages.iterator().next());
218                } else {
219                    sb.append("<ul>");
220                    for (String msg: errorMessages) {
221                        sb.append("<li>").append(msg).append("</li>");
222                    }
223                    sb.append("/ul>");
224                }
225                sb.append("</html>");
226                return sb.toString();
227            }
228    
229            @Override protected void finish() {
230                if (!errorMessages.isEmpty()) {
231                    JOptionPane.showMessageDialog(
232                            Main.parent,
233                            formatErrorMessages(),
234                            tr("Error"),
235                            JOptionPane.ERROR_MESSAGE
236                            );
237                }
238                if (layer != null) {
239                    Main.main.addLayer(layer);
240                    layer.hook_up_mouse_events(); // Main.map.mapView should exist
241                    // now. Can add mouse listener
242                    Main.map.mapView.addPropertyChangeListener(layer);
243                    if (Main.map.getToggleDialog(ImageViewerDialog.class) == null) {
244                        ImageViewerDialog.newInstance();
245                        Main.map.addToggleDialog(ImageViewerDialog.getInstance());
246                    }
247    
248                    if (! canceled && layer.data.size() > 0) {
249                        boolean noGeotagFound = true;
250                        for (ImageEntry e : layer.data) {
251                            if (e.getPos() != null) {
252                                noGeotagFound = false;
253                            }
254                        }
255                        if (noGeotagFound) {
256                            new CorrelateGpxWithImages(layer).actionPerformed(null);
257                        }
258                    }
259                }
260            }
261    
262            @Override protected void cancel() {
263                canceled = true;
264            }
265        }
266    
267        public static void create(Collection<File> files, GpxLayer gpxLayer) {
268            Loader loader = new Loader(files, gpxLayer);
269            Main.worker.execute(loader);
270        }
271    
272        private GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer) {
273    
274            super(tr("Geotagged Images"));
275    
276            Collections.sort(data);
277            this.data = data;
278            this.gpxLayer = gpxLayer;
279        }
280    
281        @Override
282        public Icon getIcon() {
283            return ImageProvider.get("dialogs/geoimage");
284        }
285    
286        private static List<Action> menuAdditions = new LinkedList<Action>();
287        public static void registerMenuAddition(Action addition) {
288            menuAdditions.add(addition);
289        }
290    
291        @Override
292        public Action[] getMenuEntries() {
293    
294            List<Action> entries = new ArrayList<Action>();
295            entries.add(LayerListDialog.getInstance().createShowHideLayerAction());
296            entries.add(LayerListDialog.getInstance().createDeleteLayerAction());
297            entries.add(new RenameLayerAction(null, this));
298            entries.add(SeparatorLayerAction.INSTANCE);
299            entries.add(new CorrelateGpxWithImages(this));
300            if (!menuAdditions.isEmpty()) {
301                entries.add(SeparatorLayerAction.INSTANCE);
302                entries.addAll(menuAdditions);
303            }
304            entries.add(SeparatorLayerAction.INSTANCE);
305            entries.add(new JumpToNextMarker(this));
306            entries.add(new JumpToPreviousMarker(this));
307            entries.add(SeparatorLayerAction.INSTANCE);
308            entries.add(new LayerListPopup.InfoAction(this));
309    
310            return entries.toArray(new Action[0]);
311    
312        }
313    
314        private String infoText() {
315            int i = 0;
316            for (ImageEntry e : data)
317                if (e.getPos() != null) {
318                    i++;
319                }
320            return trn("{0} image loaded.", "{0} images loaded.", data.size(), data.size())
321                    + " " + trn("{0} was found to be GPS tagged.", "{0} were found to be GPS tagged.", i, i);
322        }
323    
324        @Override public Object getInfoComponent() {
325            return infoText();
326        }
327    
328        @Override
329        public String getToolTipText() {
330            return infoText();
331        }
332    
333        @Override
334        public boolean isMergable(Layer other) {
335            return other instanceof GeoImageLayer;
336        }
337    
338        @Override
339        public void mergeFrom(Layer from) {
340            GeoImageLayer l = (GeoImageLayer) from;
341    
342            ImageEntry selected = null;
343            if (l.currentPhoto >= 0) {
344                selected = l.data.get(l.currentPhoto);
345            }
346    
347            data.addAll(l.data);
348            Collections.sort(data);
349    
350            // Supress the double photos.
351            if (data.size() > 1) {
352                ImageEntry cur;
353                ImageEntry prev = data.get(data.size() - 1);
354                for (int i = data.size() - 2; i >= 0; i--) {
355                    cur = data.get(i);
356                    if (cur.getFile().equals(prev.getFile())) {
357                        data.remove(i);
358                    } else {
359                        prev = cur;
360                    }
361                }
362            }
363    
364            if (selected != null) {
365                for (int i = 0; i < data.size() ; i++) {
366                    if (data.get(i) == selected) {
367                        currentPhoto = i;
368                        ImageViewerDialog.showImage(GeoImageLayer.this, data.get(i));
369                        break;
370                    }
371                }
372            }
373    
374            setName(l.getName());
375        }
376    
377        private Dimension scaledDimension(Image thumb) {
378            final double d = Main.map.mapView.getDist100Pixel();
379            final double size = 10 /*meter*/;     /* size of the photo on the map */
380            double s = size * 100 /*px*/ / d;
381    
382            final double sMin = ThumbsLoader.minSize;
383            final double sMax = ThumbsLoader.maxSize;
384    
385            if (s < sMin) {
386                s = sMin;
387            }
388            if (s > sMax) {
389                s = sMax;
390            }
391            final double f = s / sMax;  /* scale factor */
392    
393            if (thumb == null)
394                return null;
395    
396            return new Dimension(
397                    (int) Math.round(f * thumb.getWidth(null)),
398                    (int) Math.round(f * thumb.getHeight(null)));
399        }
400    
401        @Override
402        public void paint(Graphics2D g, MapView mv, Bounds bounds) {
403            int width = Main.map.mapView.getWidth();
404            int height = Main.map.mapView.getHeight();
405            Rectangle clip = g.getClipBounds();
406            if (useThumbs) {
407                if (null == offscreenBuffer || offscreenBuffer.getWidth() != width  // reuse the old buffer if possible
408                        || offscreenBuffer.getHeight() != height) {
409                    offscreenBuffer = new BufferedImage(width, height,
410                            BufferedImage.TYPE_INT_ARGB);
411                    updateOffscreenBuffer = true;
412                }
413    
414                if (updateOffscreenBuffer) {
415                    Graphics2D tempG = offscreenBuffer.createGraphics();
416                    tempG.setColor(new Color(0,0,0,0));
417                    Composite saveComp = tempG.getComposite();
418                    tempG.setComposite(AlphaComposite.Clear);   // remove the old images
419                    tempG.fillRect(0, 0, width, height);
420                    tempG.setComposite(saveComp);
421    
422                    for (ImageEntry e : data) {
423                        if (e.getPos() == null) {
424                            continue;
425                        }
426                        Point p = mv.getPoint(e.getPos());
427                        if (e.thumbnail != null) {
428                            Dimension d = scaledDimension(e.thumbnail);
429                            Rectangle target = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
430                            if (clip.intersects(target)) {
431                                tempG.drawImage(e.thumbnail, target.x, target.y, target.width, target.height, null);
432                            }
433                        }
434                        else { // thumbnail not loaded yet
435                            icon.paintIcon(mv, tempG,
436                                    p.x - icon.getIconWidth() / 2,
437                                    p.y - icon.getIconHeight() / 2);
438                        }
439                    }
440                    updateOffscreenBuffer = false;
441                }
442                g.drawImage(offscreenBuffer, 0, 0, null);
443            }
444            else {
445                for (ImageEntry e : data) {
446                    if (e.getPos() == null) {
447                        continue;
448                    }
449                    Point p = mv.getPoint(e.getPos());
450                    icon.paintIcon(mv, g,
451                            p.x - icon.getIconWidth() / 2,
452                            p.y - icon.getIconHeight() / 2);
453                }
454            }
455    
456            if (currentPhoto >= 0 && currentPhoto < data.size()) {
457                ImageEntry e = data.get(currentPhoto);
458    
459                if (e.getPos() != null) {
460                    Point p = mv.getPoint(e.getPos());
461    
462                    if (e.thumbnail != null) {
463                        Dimension d = scaledDimension(e.thumbnail);
464                        g.setColor(new Color(128, 0, 0, 122));
465                        g.fillRect(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
466                    } else {
467                        if (e.getExifImgDir() != null) {
468                            double arrowlength = 25;
469                            double arrowwidth = 18;
470    
471                            double dir = e.getExifImgDir();
472                            // Rotate 90 degrees CCW
473                            double headdir = ( dir < 90 ) ? dir + 270 : dir - 90;
474                            double leftdir = ( headdir < 90 ) ? headdir + 270 : headdir - 90;
475                            double rightdir = ( headdir > 270 ) ? headdir - 270 : headdir + 90;
476    
477                            double ptx = p.x + Math.cos(Math.toRadians(headdir)) * arrowlength;
478                            double pty = p.y + Math.sin(Math.toRadians(headdir)) * arrowlength;
479    
480                            double ltx = p.x + Math.cos(Math.toRadians(leftdir)) * arrowwidth/2;
481                            double lty = p.y + Math.sin(Math.toRadians(leftdir)) * arrowwidth/2;
482    
483                            double rtx = p.x + Math.cos(Math.toRadians(rightdir)) * arrowwidth/2;
484                            double rty = p.y + Math.sin(Math.toRadians(rightdir)) * arrowwidth/2;
485    
486                            g.setColor(Color.white);
487                            int[] xar = {(int) ltx, (int) ptx, (int) rtx, (int) ltx};
488                            int[] yar = {(int) lty, (int) pty, (int) rty, (int) lty};
489                            g.fillPolygon(xar, yar, 4);
490                        }
491    
492                        selectedIcon.paintIcon(mv, g,
493                                p.x - selectedIcon.getIconWidth() / 2,
494                                p.y - selectedIcon.getIconHeight() / 2);
495    
496                    }
497                }
498            }
499        }
500    
501        @Override
502        public void visitBoundingBox(BoundingXYVisitor v) {
503            for (ImageEntry e : data) {
504                v.visit(e.getPos());
505            }
506        }
507    
508        /*
509         * Extract gps from image exif
510         *
511         * If successful, fills in the LatLon and EastNorth attributes of passed in
512         * image;
513         */
514    
515        private static void extractExif(ImageEntry e) {
516    
517            double deg;
518            double min, sec;
519            double lon, lat;
520            Metadata metadata = null;
521            Directory dirExif = null, dirGps = null;
522    
523            try {
524                metadata = JpegMetadataReader.readMetadata(e.getFile());
525                dirExif = metadata.getDirectory(ExifDirectory.class);
526                dirGps = metadata.getDirectory(GpsDirectory.class);
527            } catch (CompoundException p) {
528                e.setExifCoor(null);
529                e.setPos(null);
530                return;
531            }
532    
533            try {
534                int orientation = dirExif.getInt(ExifDirectory.TAG_ORIENTATION);
535                e.setExifOrientation(orientation);
536            } catch (MetadataException ex) {
537            }
538    
539            try {
540                double ele=dirGps.getDouble(GpsDirectory.TAG_GPS_ALTITUDE);
541                int d = dirGps.getInt(GpsDirectory.TAG_GPS_ALTITUDE_REF);
542                if (d == 1) {
543                    ele *= -1;
544                }
545                e.setElevation(ele);
546            } catch (MetadataException ex) {
547            }
548    
549            try {
550                // longitude
551    
552                Rational[] components = dirGps.getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
553    
554                deg = components[0].doubleValue();
555                min = components[1].doubleValue();
556                sec = components[2].doubleValue();
557    
558                if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
559                    throw new IllegalArgumentException();
560    
561                lon = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)));
562    
563                if (dirGps.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF).charAt(0) == 'W') {
564                    lon = -lon;
565                }
566    
567                // latitude
568    
569                components = dirGps.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
570    
571                deg = components[0].doubleValue();
572                min = components[1].doubleValue();
573                sec = components[2].doubleValue();
574    
575                if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
576                    throw new IllegalArgumentException();
577    
578                lat = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)));
579    
580                if (Double.isNaN(lat))
581                    throw new IllegalArgumentException();
582    
583                if (dirGps.getString(GpsDirectory.TAG_GPS_LATITUDE_REF).charAt(0) == 'S') {
584                    lat = -lat;
585                }
586    
587                // Store values
588    
589                e.setExifCoor(new LatLon(lat, lon));
590                e.setPos(e.getExifCoor());
591    
592            } catch (CompoundException p) {
593                // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
594                try {
595                    Double longitude = dirGps.getDouble(GpsDirectory.TAG_GPS_LONGITUDE);
596                    Double latitude = dirGps.getDouble(GpsDirectory.TAG_GPS_LATITUDE);
597                    if (longitude == null || latitude == null)
598                        throw new CompoundException("");
599    
600                    // Store values
601    
602                    e.setExifCoor(new LatLon(latitude, longitude));
603                    e.setPos(e.getExifCoor());
604                } catch (CompoundException ex) {
605                    e.setExifCoor(null);
606                    e.setPos(null);
607                }
608            } catch (Exception ex) { // (other exceptions, e.g. #5271)
609                System.err.println("Error reading EXIF from file: "+ex);
610                e.setExifCoor(null);
611                e.setPos(null);
612            }
613    
614            // compass direction value
615    
616            Rational direction = null;
617    
618            try {
619                direction = dirGps.getRational(GpsDirectory.TAG_GPS_IMG_DIRECTION);
620                if (direction != null) {
621                    e.setExifImgDir(direction.doubleValue());
622                }
623            } catch (Exception ex) { // (CompoundException and other exceptions, e.g. #5271)
624                // Do nothing
625            }
626        }
627    
628        public void showNextPhoto() {
629            if (data != null && data.size() > 0) {
630                currentPhoto++;
631                if (currentPhoto >= data.size()) {
632                    currentPhoto = data.size() - 1;
633                }
634                ImageViewerDialog.showImage(this, data.get(currentPhoto));
635            } else {
636                currentPhoto = -1;
637            }
638            Main.map.repaint();
639        }
640    
641        public void showPreviousPhoto() {
642            if (data != null && data.size() > 0) {
643                currentPhoto--;
644                if (currentPhoto < 0) {
645                    currentPhoto = 0;
646                }
647                ImageViewerDialog.showImage(this, data.get(currentPhoto));
648            } else {
649                currentPhoto = -1;
650            }
651            Main.map.repaint();
652        }
653    
654        public void checkPreviousNextButtons() {
655            ImageViewerDialog.setNextEnabled(currentPhoto < data.size() - 1);
656            ImageViewerDialog.setPreviousEnabled(currentPhoto > 0);
657        }
658    
659        public void removeCurrentPhoto() {
660            if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) {
661                data.remove(currentPhoto);
662                if (currentPhoto >= data.size()) {
663                    currentPhoto = data.size() - 1;
664                }
665                if (currentPhoto >= 0) {
666                    ImageViewerDialog.showImage(this, data.get(currentPhoto));
667                } else {
668                    ImageViewerDialog.showImage(this, null);
669                }
670                updateOffscreenBuffer = true;
671                Main.map.repaint();
672            }
673        }
674    
675        public void removeCurrentPhotoFromDisk() {
676            ImageEntry toDelete = null;
677            if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) {
678                toDelete = data.get(currentPhoto);
679    
680                int result = new ExtendedDialog(
681                        Main.parent,
682                        tr("Delete image file from disk"),
683                        new String[] {tr("Cancel"), tr("Delete")})
684                .setButtonIcons(new String[] {"cancel.png", "dialogs/delete.png"})
685                .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>"
686                        ,toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"),SwingConstants.LEFT))
687                        .toggleEnable("geoimage.deleteimagefromdisk")
688                        .setCancelButton(1)
689                        .setDefaultButton(2)
690                        .showDialog()
691                        .getValue();
692    
693                if(result == 2)
694                {
695                    data.remove(currentPhoto);
696                    if (currentPhoto >= data.size()) {
697                        currentPhoto = data.size() - 1;
698                    }
699                    if (currentPhoto >= 0) {
700                        ImageViewerDialog.showImage(this, data.get(currentPhoto));
701                    } else {
702                        ImageViewerDialog.showImage(this, null);
703                    }
704    
705                    if (toDelete.getFile().delete()) {
706                        System.out.println("File "+toDelete.getFile().toString()+" deleted. ");
707                    } else {
708                        JOptionPane.showMessageDialog(
709                                Main.parent,
710                                tr("Image file could not be deleted."),
711                                tr("Error"),
712                                JOptionPane.ERROR_MESSAGE
713                                );
714                    }
715    
716                    updateOffscreenBuffer = true;
717                    Main.map.repaint();
718                }
719            }
720        }
721    
722        private MouseAdapter mouseAdapter = null;
723        private MapModeChangeListener mapModeListener = null;
724    
725        private void hook_up_mouse_events() {
726            mouseAdapter = new MouseAdapter() {
727                private final boolean isMapModeOk() {
728                    return Main.map.mapMode == null || Main.map.mapMode instanceof SelectAction;
729                }
730                @Override public void mousePressed(MouseEvent e) {
731    
732                    if (e.getButton() != MouseEvent.BUTTON1)
733                        return;
734                    if (isVisible() && isMapModeOk()) {
735                        Main.map.mapView.repaint();
736                    }
737                }
738    
739                @Override public void mouseReleased(MouseEvent ev) {
740                    if (ev.getButton() != MouseEvent.BUTTON1)
741                        return;
742                    if (data == null || !isVisible() || !isMapModeOk())
743                        return;
744    
745                    for (int i = data.size() - 1; i >= 0; --i) {
746                        ImageEntry e = data.get(i);
747                        if (e.getPos() == null) {
748                            continue;
749                        }
750                        Point p = Main.map.mapView.getPoint(e.getPos());
751                        Rectangle r;
752                        if (e.thumbnail != null) {
753                            Dimension d = scaledDimension(e.thumbnail);
754                            r = new Rectangle(p.x - d.width / 2, p.y - d.height / 2, d.width, d.height);
755                        } else {
756                            r = new Rectangle(p.x - icon.getIconWidth() / 2,
757                                    p.y - icon.getIconHeight() / 2,
758                                    icon.getIconWidth(),
759                                    icon.getIconHeight());
760                        }
761                        if (r.contains(ev.getPoint())) {
762                            currentPhoto = i;
763                            ImageViewerDialog.showImage(GeoImageLayer.this, e);
764                            Main.map.repaint();
765                            break;
766                        }
767                    }
768                }
769            };
770    
771            mapModeListener = new MapModeChangeListener() {
772                public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) {
773                    if (newMapMode == null || (newMapMode instanceof org.openstreetmap.josm.actions.mapmode.SelectAction)) {
774                        Main.map.mapView.addMouseListener(mouseAdapter);
775                    } else {
776                        Main.map.mapView.removeMouseListener(mouseAdapter);
777                    }
778                }
779            };
780    
781            MapFrame.addMapModeChangeListener(mapModeListener);
782            mapModeListener.mapModeChange(null, Main.map.mapMode);
783    
784            MapView.addLayerChangeListener(new LayerChangeListener() {
785                public void activeLayerChange(Layer oldLayer, Layer newLayer) {
786                    if (newLayer == GeoImageLayer.this) {
787                        // only in select mode it is possible to click the images
788                        Main.map.selectSelectTool(false);
789                    }
790                }
791    
792                public void layerAdded(Layer newLayer) {
793                }
794    
795                public void layerRemoved(Layer oldLayer) {
796                    if (oldLayer == GeoImageLayer.this) {
797                        if (thumbsloader != null) {
798                            thumbsloader.stop = true;
799                        }
800                        Main.map.mapView.removeMouseListener(mouseAdapter);
801                        MapFrame.removeMapModeChangeListener(mapModeListener);
802                        currentPhoto = -1;
803                        data.clear();
804                        data = null;
805                        // stop listening to layer change events
806                        MapView.removeLayerChangeListener(this);
807                    }
808                }
809            });
810        }
811    
812        public void propertyChange(PropertyChangeEvent evt) {
813            if (NavigatableComponent.PROPNAME_CENTER.equals(evt.getPropertyName()) || NavigatableComponent.PROPNAME_SCALE.equals(evt.getPropertyName())) {
814                updateOffscreenBuffer = true;
815            }
816        }
817    
818        public void loadThumbs() {
819            if (useThumbs && !thumbsLoaded) {
820                thumbsLoaded = true;
821                thumbsloader = new ThumbsLoader(this);
822                Thread t = new Thread(thumbsloader);
823                t.setPriority(Thread.MIN_PRIORITY);
824                t.start();
825            }
826        }
827    
828        public void updateBufferAndRepaint() {
829            updateOffscreenBuffer = true;
830            Main.map.mapView.repaint();
831        }
832    
833        public List<ImageEntry> getImages() {
834            List<ImageEntry> copy = new ArrayList<ImageEntry>();
835            for (ImageEntry ie : data) {
836                copy.add(ie.clone());
837            }
838            return copy;
839        }
840    
841        public void jumpToNextMarker() {
842            showNextPhoto();
843        }
844    
845        public void jumpToPreviousMarker() {
846            showPreviousPhoto();
847        }
848    }