Changeset 2566 in josm for trunk/src/org


Ignore:
Timestamp:
2009-12-03T22:15:17+01:00 (15 years ago)
Author:
bastiK
Message:

Moved the code from agpifoj plugin to JOSM core. Thanks to Christian Gallioz for this great (ex-)plugin.
In the current state it might be a little unstable.

  • Did a view modification so it fits in better.
  • Added the Thumbnail feature, but still work to be done.

New in JOSM core: Possibility to add toggle dialogs not only on startup, but also later.

Location:
trunk/src/org/openstreetmap/josm/gui
Files:
5 added
4 edited
1 moved

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/gui/MapFrame.java

    r2391 r2566  
    8080     */
    8181    private List<ToggleDialog> allDialogs = new ArrayList<ToggleDialog>();
    82     private DialogsPanel dialogsPanel;
     82    private final DialogsPanel dialogsPanel;
    8383
    8484    public final ButtonGroup toolGroup = new ButtonGroup();
     
    219219        toolBarToggle.add(button);
    220220        allDialogs.add(dlg);
     221        if (dialogsPanel.initialized) {
     222            dialogsPanel.add(dlg);
     223        }
    221224        return button;
    222225    }
  • trunk/src/org/openstreetmap/josm/gui/dialogs/DialogsPanel.java

    • Property svn:eol-style set to native
    r2525 r2566  
    3232    }
    3333
    34     private boolean initialized = false;
    35     public void initialize(List<ToggleDialog> allDialogs) {
     34    public boolean initialized = false; // read only from outside
     35   
     36    public void initialize(List<ToggleDialog> pAllDialogs) {
    3637        if (initialized)
    3738            throw new IllegalStateException();
    3839        initialized = true;
    39         this.allDialogs = allDialogs;
    40 
    41         for (Integer i=0; i < allDialogs.size(); ++i) {
    42             final ToggleDialog dlg = allDialogs.get(i);
    43             dlg.setDialogsPanel(this);
    44             dlg.setVisible(false);
    45         }
    46         for (int i=0; i < allDialogs.size() + 1; ++i) {
    47             final JPanel p = new JPanel() {
    48                 /**
    49                  * Honoured by the MultiSplitPaneLayout when the
    50                  * entire Window is resized.
    51                  */
    52                 @Override
    53                 public Dimension getMinimumSize() {
    54                     return new Dimension(0, 40);
    55                 }
    56             };
    57             p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
    58             p.setVisible(false);
    59 
    60             mSpltPane.add(p, "L"+i);
    61             panels.add(p);
    62         }
    63 
    64         for (Integer i=0; i < allDialogs.size(); ++i) {
    65             final ToggleDialog dlg = allDialogs.get(i);
    66             if (dlg.isDialogShowing()) {
    67                 dlg.showDialog();
    68                 if (dlg.isDialogInCollapsedView()) {
    69                     dlg.isCollapsed = false;    // pretend to be in Default view, this will be set back by collapse()
    70                     dlg.collapse();
    71                 }
    72             } else {
    73                 dlg.hideDialog();
    74             }
    75         }
     40        allDialogs = new ArrayList<ToggleDialog>();
     41
     42        for (Integer i=0; i < pAllDialogs.size(); ++i) {
     43            add(pAllDialogs.get(i), false);
     44        }
     45       
    7646        this.add(mSpltPane);
    7747        reconstruct(Action.ELEMENT_SHRINKS, null);
     48    }
     49
     50    public void add(ToggleDialog dlg) {
     51        add(dlg, true);
     52    }
     53   
     54    public void add(ToggleDialog dlg, boolean doReconstruct) {
     55        allDialogs.add(dlg);
     56        int i = allDialogs.size() - 1;
     57        dlg.setDialogsPanel(this);
     58        dlg.setVisible(false);
     59        final JPanel p = new JPanel() {
     60            /**
     61             * Honoured by the MultiSplitPaneLayout when the
     62             * entire Window is resized.
     63             */
     64            @Override
     65            public Dimension getMinimumSize() {
     66                return new Dimension(0, 40);
     67            }
     68        };
     69        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
     70        p.setVisible(false);
     71
     72        mSpltPane.add(p, "L"+i);
     73        panels.add(p);
     74
     75        if (dlg.isDialogShowing()) {
     76            dlg.showDialog();
     77            if (dlg.isDialogInCollapsedView()) {
     78                dlg.isCollapsed = false;    // pretend to be in Default view, this will be set back by collapse()
     79                dlg.collapse();
     80            }
     81            if (doReconstruct) {
     82                reconstruct(Action.INVISIBLE_TO_DEFAULT, dlg);
     83            }
     84        } else {
     85            dlg.hideDialog();
     86        }
    7887    }
    7988
  • trunk/src/org/openstreetmap/josm/gui/dialogs/ToggleDialog.java

    r2525 r2566  
     1// License: GPL. See LICENSE file for details.
    12package org.openstreetmap.josm.gui.dialogs;
    23
  • trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java

    r2549 r2566  
    6464import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    6565import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
     66import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer;
    6667import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker;
    6768import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
     
    298299        });
    299300
    300         JMenuItem tagimage = new JMenuItem(tr("Import images"), ImageProvider.get("tagimages"));
     301        JMenuItem tagimage = new JMenuItem(tr("Import images"), ImageProvider.get("dialogs/geoimage"));
    301302        tagimage.putClientProperty("help", ht("/Action/ImportImages"));
    302303        tagimage.addActionListener(new ActionListener() {
  • trunk/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java

    r2564 r2566  
    1 //License: GPL. Copyright 2007 by Immanuel Scholz and others
    2 package org.openstreetmap.josm.gui.layer;
     1// License: GPL. See LICENSE file for details.
     2// Copyright 2007 by Christian Gallioz (aka khris78)
     3// Parts of code from Geotagged plugin (by Rob Neild)
     4// and the core JOSM source code (by Immanuel Scholz and others)
     5
     6package org.openstreetmap.josm.gui.layer.geoimage;
    37
    48import static org.openstreetmap.josm.tools.I18n.tr;
    59import static org.openstreetmap.josm.tools.I18n.trn;
    610
    7 import java.awt.BorderLayout;
    8 import java.awt.Color;
    911import java.awt.Component;
    10 import java.awt.Cursor;
    1112import java.awt.Graphics2D;
    12 import java.awt.GridBagLayout;
    1313import java.awt.Image;
    14 import java.awt.Insets;
     14import java.awt.MediaTracker;
    1515import java.awt.Point;
    1616import java.awt.Rectangle;
    1717import java.awt.Toolkit;
    18 import java.awt.event.ActionEvent;
    19 import java.awt.event.ActionListener;
    20 import java.awt.event.ComponentEvent;
    21 import java.awt.event.ComponentListener;
    22 import java.awt.event.KeyEvent;
    2318import java.awt.event.MouseAdapter;
    2419import java.awt.event.MouseEvent;
    2520import java.awt.image.BufferedImage;
    26 import java.awt.image.ImageObserver;
    2721import java.io.File;
    2822import java.io.IOException;
    29 import java.lang.ref.SoftReference;
    3023import java.text.ParseException;
    31 import java.text.SimpleDateFormat;
    3224import java.util.ArrayList;
     25import java.util.Arrays;
     26import java.util.Collections;
    3327import java.util.Collection;
    34 import java.util.Collections;
    3528import java.util.Date;
    36 import java.util.LinkedList;
     29import java.util.LinkedHashSet;
     30import java.util.HashSet;
    3731import java.util.List;
    38 import java.util.WeakHashMap;
    39 
    40 import javax.swing.BorderFactory;
    41 import javax.swing.DefaultListCellRenderer;
     32
    4233import javax.swing.Icon;
    43 import javax.swing.ImageIcon;
    44 import javax.swing.JButton;
    45 import javax.swing.JDialog;
    46 import javax.swing.JFileChooser;
    47 import javax.swing.JLabel;
    48 import javax.swing.JList;
    4934import javax.swing.JMenuItem;
    5035import javax.swing.JOptionPane;
    51 import javax.swing.JPanel;
    52 import javax.swing.JScrollPane;
    5336import javax.swing.JSeparator;
    54 import javax.swing.JTextField;
    55 import javax.swing.JToggleButton;
    56 import javax.swing.JViewport;
    57 import javax.swing.ScrollPaneConstants;
    58 import javax.swing.border.BevelBorder;
    59 import javax.swing.border.Border;
    60 import javax.swing.filechooser.FileFilter;
    6137
    6238import org.openstreetmap.josm.Main;
     
    6541import org.openstreetmap.josm.data.coor.CachedLatLon;
    6642import org.openstreetmap.josm.data.coor.LatLon;
    67 import org.openstreetmap.josm.data.gpx.GpxTrack;
    68 import org.openstreetmap.josm.data.gpx.WayPoint;
    6943import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    70 import org.openstreetmap.josm.gui.MapFrame;
    7144import org.openstreetmap.josm.gui.MapView;
    7245import org.openstreetmap.josm.gui.PleaseWaitRunnable;
    7346import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    74 import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    75 import org.openstreetmap.josm.gui.layer.GeoImageLayer.ImageLoader.ImageLoadedListener;
    76 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    77 import org.openstreetmap.josm.tools.DateParser;
     47import org.openstreetmap.josm.gui.layer.GpxLayer;
     48import org.openstreetmap.josm.gui.layer.Layer;
    7849import org.openstreetmap.josm.tools.ExifReader;
    79 import org.openstreetmap.josm.tools.GBC;
    8050import org.openstreetmap.josm.tools.ImageProvider;
    8151
    82 /**
    83  * A layer which imports several photos from disk and read EXIF time information from them.
    84  *
    85  * @author Imi
    86  */
     52import com.drew.imaging.jpeg.JpegMetadataReader;
     53import com.drew.lang.Rational;
     54import com.drew.metadata.Directory;
     55import com.drew.metadata.Metadata;
     56import com.drew.metadata.exif.GpsDirectory;
     57
    8758public class GeoImageLayer extends Layer {
    8859
    89     /**
    90      * Allows to load and scale images. Loaded images are kept in cache (using soft reference). Both
    91      * synchronous and asynchronous loading is supported
    92      *
     60    List<ImageEntry> data;
     61
     62    private Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
     63    private Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
     64
     65    private int currentPhoto = -1;
     66
     67    // These are used by the auto-guess function to store the result,
     68    // so when the dialig is re-opened the users modifications don't
     69    // get overwritten
     70    public boolean hasTimeoffset = false;
     71    public long timeoffset = 0;
     72
     73    /*
     74     * Stores info about each image
    9375     */
    94     public static class ImageLoader implements ImageObserver {
    95 
    96         public static class Entry {
    97             final File file;
    98             final int width;
    99             final int height;
    100             final int maxSize;
    101             private final ImageLoadedListener listener;
    102 
    103             volatile Image scaledImage;
    104 
    105             public Entry(File file, int width, int height, int maxSize, ImageLoadedListener listener) {
    106                 this.file = file;
    107                 this.height = height;
    108                 this.width = width;
    109                 this.maxSize = maxSize;
    110                 this.listener = listener;
    111             }
    112         }
    113 
    114         public interface ImageLoadedListener {
    115             void imageLoaded();
    116         }
    117 
    118         private final List<ImageLoader.Entry> queue = new ArrayList<ImageLoader.Entry>();
    119         private final WeakHashMap<File, SoftReference<Image>> loadedImageCache = new WeakHashMap<File, SoftReference<Image>>();
    120         private ImageLoader.Entry currentEntry;
    121 
    122         private Image getOrLoadImage(File file) {
    123             SoftReference<Image> cachedImageRef = loadedImageCache.get(file);
    124             if (cachedImageRef != null) {
    125                 Image cachedImage = cachedImageRef.get();
    126                 if (cachedImage != null)
    127                     return cachedImage;
    128             }
    129             return Toolkit.getDefaultToolkit().createImage(currentEntry.file.getAbsolutePath());
    130         }
    131 
    132         private BufferedImage createResizedCopy(Image originalImage,
    133                 int scaledWidth, int scaledHeight)
     76
     77    static final class ImageEntry implements Comparable<ImageEntry> {
     78        File file;
     79        Date time;
     80        LatLon exifCoor;
     81        CachedLatLon pos;
     82        Image thumbnail;
     83        /** Speed in kilometer per second */
     84        Double speed;
     85        /** Elevation (altitude) in meters */
     86        Double elevation;
     87
     88        public void setCoor(LatLon latlon)
    13489        {
    135             BufferedImage scaledBI = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
    136             Graphics2D g = scaledBI.createGraphics();
    137             while (!g.drawImage(originalImage, 0, 0, scaledWidth, scaledHeight, null))
    138             {
    139                 try {
    140                     Thread.sleep(10);
    141                 } catch(InterruptedException ie) {}
    142             }
    143             g.dispose();
    144             return scaledBI;
    145         }
    146         private void loadImage() {
    147             if (currentEntry != null)
     90            pos = new CachedLatLon(latlon);
     91        }
     92        public int compareTo(ImageEntry image) {
     93            if (time != null && image.time != null) {
     94                return time.compareTo(image.time);
     95            } else if (time == null && image.time == null) {
     96                return 0;
     97            } else if (time == null) {
     98                return -1;
     99            } else {
     100                return 1;
     101            }
     102        }
     103    }
     104
     105    /** Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing.
     106     * In facts, this object is instantiated with a list of files. These files may be JPEG files or
     107     * directories. In case of directories, they are scanned to find all the images they contain.
     108     * Then all the images that have be found are loaded as ImageEntry instances.
     109     */
     110    private static final class Loader extends PleaseWaitRunnable {
     111
     112        private boolean cancelled = false;
     113        private GeoImageLayer layer;
     114        private Collection<File> selection;
     115        private HashSet<String> loadedDirectories = new HashSet<String>();
     116        private LinkedHashSet<String> errorMessages;
     117
     118        protected void rememberError(String message) {
     119                this.errorMessages.add(message);
     120        }
     121
     122        public Loader(Collection<File> selection, GpxLayer gpxLayer) {
     123            super(tr("Extracting GPS locations from EXIF"));
     124            this.selection = selection;
     125            errorMessages = new LinkedHashSet<String>();
     126        }
     127
     128        @Override protected void realRun() throws IOException {
     129
     130            progressMonitor.subTask(tr("Starting directory scan"));
     131            Collection<File> files = new ArrayList<File>();
     132            try {
     133                addRecursiveFiles(files, selection);
     134            } catch(NullPointerException npe) {
     135                rememberError(tr("One of the selected files was null"));
     136            }
     137
     138            if (cancelled) {
    148139                return;
    149             while (!queue.isEmpty()) {
    150                 currentEntry = queue.get(0);
    151                 queue.remove(0);
    152 
    153                 Image newImage = getOrLoadImage(currentEntry.file);
    154                 if (newImage.getWidth(this) == -1) {
     140            }           
     141            progressMonitor.subTask(tr("Read photos..."));
     142            progressMonitor.setTicksCount(files.size());
     143
     144            progressMonitor.subTask(tr("Read photos..."));
     145            progressMonitor.setTicksCount(files.size());
     146
     147            // read the image files
     148            List<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
     149
     150            for (File f : files) {
     151
     152                if (cancelled) {
    155153                    break;
    156                 } else {
    157                     finishImage(newImage, currentEntry);
    158                     currentEntry = null;
    159                 }
    160             }
    161         }
    162 
    163         private void finishImage(Image img, ImageLoader.Entry entry) {
    164             loadedImageCache.put(entry.file, new SoftReference<Image>(img));
    165             if (entry.maxSize != -1) {
    166                 int w = img.getWidth(null);
    167                 int h = img.getHeight(null);
    168                 if (w>h) {
    169                     h = Math.round(entry.maxSize*((float)h/w));
    170                     w = entry.maxSize;
    171                 } else {
    172                     w = Math.round(entry.maxSize*((float)w/h));
    173                     h = entry.maxSize;
    174                 }
    175                 entry.scaledImage = createResizedCopy(img, w, h);
    176             } else if (entry.width != -1 && entry.height != -1) {
    177                 entry.scaledImage = createResizedCopy(img, entry.width, entry.height);
    178             } else {
    179                 entry.scaledImage = img;
    180             }
    181             if (entry.listener != null) {
    182                 entry.listener.imageLoaded();
    183             }
    184         }
    185 
    186         public synchronized ImageLoader.Entry loadImage(File file, int width, int height, int maxSize, ImageLoadedListener listener) {
    187             ImageLoader.Entry e = new Entry(file, width, height, maxSize, listener);
    188             queue.add(e);
    189             loadImage();
    190             return e;
    191         }
    192 
    193         public Image waitForImage(File file, int width, int height) {
    194             return waitForImage(file, width, height, -1);
    195         }
    196 
    197         public Image waitForImage(File file, int maxSize) {
    198             return waitForImage(file, -1, -1, maxSize);
    199         }
    200 
    201         public Image waitForImage(File file) {
    202             return waitForImage(file, -1, -1, -1);
    203         }
    204 
    205         private synchronized Image waitForImage(File file, int width, int height, int maxSize) {
    206             ImageLoader.Entry entry;
    207             if (currentEntry != null && currentEntry.file.equals(file)) {
    208                 entry = currentEntry;
    209             } else {
    210                 entry = new Entry(file, width, height, maxSize, null);
    211                 queue.add(0, entry);
    212             }
    213             loadImage();
    214 
    215             while (true) {
    216                 if (entry.scaledImage != null)
    217                     return entry.scaledImage;
    218                 try {
    219                     wait();
    220                 } catch (InterruptedException e) {}
    221             }
    222         }
    223 
    224         public synchronized boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
    225             if ((infoflags & ImageObserver.ALLBITS) != 0) {
    226                 finishImage(img, currentEntry);
    227                 currentEntry = null;
    228                 loadImage();
    229                 notifyAll();
    230             } else if ((infoflags & ImageObserver.ERROR) != 0) {
    231                 currentEntry.scaledImage = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY);
    232                 currentEntry = null;
    233             }
    234             return true;
    235         }
    236     }
    237 
    238     private static final int ICON_SIZE = 16;
    239     private static ImageLoader imageLoader = new ImageLoader();
    240 
    241     private static final class ImageEntry implements Comparable<ImageEntry>, ImageLoadedListener {
    242 
    243         private static final Image EMPTY_IMAGE = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY);
    244 
    245         final File image;
    246         ImageLoader.Entry icon;
    247         Date time;
    248         CachedLatLon pos;
    249 
    250         public ImageEntry(File image) {
    251             this.image = image;
    252             icon = imageLoader.loadImage(image, ICON_SIZE, ICON_SIZE, -1, this);
    253         }
    254 
    255         public int compareTo(ImageEntry image) {
    256             return time.compareTo(image.time);
    257         }
    258 
    259         public Image getIcon() {
    260             if (icon.scaledImage == null)
    261                 return EMPTY_IMAGE;
    262             else
    263                 return icon.scaledImage;
    264         }
    265 
    266         public void imageLoaded() {
    267             MapFrame frame = Main.map;
    268             if (frame != null) {
    269                 frame.mapView.repaint();
    270             }
    271         }
    272     }
    273 
    274     private static final class Loader extends PleaseWaitRunnable {
    275         private GeoImageLayer layer;
    276         private final Collection<File> files;
    277         private final GpxLayer gpxLayer;
    278         private LinkedList<TimedPoint> gps;
    279 
    280         public Loader(Collection<File> files, GpxLayer gpxLayer) {
    281             super(tr("Images for {0}", gpxLayer.getName()));
    282             this.files = files;
    283             this.gpxLayer = gpxLayer;
    284         }
    285         @Override protected void realRun() throws IOException {
    286             progressMonitor.subTask(tr("Read GPX..."));
    287             progressMonitor.setTicksCount(10 + files.size());
    288             gps = new LinkedList<TimedPoint>();
    289 
    290             // Extract dates and locations from GPX input
    291 
    292             ProgressMonitor gpxSubTask = progressMonitor.createSubTaskMonitor(10, true);
    293             int size = 0;
    294             for (GpxTrack trk:gpxLayer.data.tracks) {
    295                 for (Collection<WayPoint> segment : trk.trackSegs) {
    296                     size += segment.size();
    297                 }
    298             }
    299             gpxSubTask.beginTask(null, size);
    300 
    301             try {
    302                 for (GpxTrack trk : gpxLayer.data.tracks) {
    303                     for (Collection<WayPoint> segment : trk.trackSegs) {
    304                         for (WayPoint p : segment) {
    305                             LatLon c = p.getCoor();
    306                             if (!p.attr.containsKey("time"))
    307                                 throw new IOException(tr("No time for point {0} x {1}",c.lat(),c.lon()));
    308                             Date d = null;
    309                             try {
    310                                 d = DateParser.parse((String) p.attr.get("time"));
    311                             } catch (ParseException e) {
    312                                 throw new IOException(tr("Cannot read time \"{0}\" from point {1} x {2}",p.attr.get("time"),c.lat(),c.lon()));
    313                             }
    314                             gps.add(new TimedPoint(d, c));
    315                             gpxSubTask.worked(1);
    316                         }
    317                     }
    318                 }
    319             } finally {
    320                 gpxSubTask.finishTask();
    321             }
    322 
    323             if (gps.isEmpty())
    324                 return;
    325 
    326             // read the image files
    327             ArrayList<ImageEntry> data = new ArrayList<ImageEntry>(files.size());
    328             for (File f : files) {
    329                 if (progressMonitor.isCancelled()) {
    330                     break;
    331                 }
    332                 progressMonitor.subTask(tr("Reading {0}...",f.getName()));
    333 
    334                 ImageEntry e = new ImageEntry(f);
     154                }
     155
     156                progressMonitor.subTask(tr("Reading {0}...", f.getName()));
     157                progressMonitor.worked(1);
     158
     159                ImageEntry e = new ImageEntry();
     160
     161                // Changed to silently cope with no time info in exif. One case
     162                // of person having time that couldn't be parsed, but valid GPS info
     163
    335164                try {
    336165                    e.time = ExifReader.readTime(f);
    337                     progressMonitor.worked(1);
    338166                } catch (ParseException e1) {
    339                     continue;
    340                 }
    341                 if (e.time == null) {
    342                     continue;
    343                 }
    344 
     167                    e.time = null;
     168                }
     169                e.file = f;
     170                extractExif(e);
    345171                data.add(e);
    346172            }
    347             layer = new GeoImageLayer(data, gps);
    348             layer.calculatePosition();
    349         }
     173            layer = new GeoImageLayer(data);
     174            files.clear();
     175            Thread thumbsloader = new Thread(new Thumbsloader());
     176            thumbsloader.setPriority(Thread.MIN_PRIORITY);
     177            thumbsloader.start();           
     178        }
     179
     180        private void addRecursiveFiles(Collection<File> files, Collection<File> sel) {
     181            boolean nullFile = false;
     182
     183            for (File f : sel) {
     184
     185                if(cancelled) {
     186                    break;
     187                }
     188
     189                if (f == null) {
     190                    nullFile = true;
     191
     192                } else if (f.isDirectory()) {
     193                    String canonical = null;
     194                    try {
     195                        canonical = f.getCanonicalPath();
     196                    } catch (IOException e) {
     197                        e.printStackTrace();
     198                        rememberError(tr("Unable to get canonical path for directory {0}\n",
     199                                           f.getAbsolutePath()));
     200                    }
     201
     202                    if (canonical == null || loadedDirectories.contains(canonical)) {
     203                        continue;
     204                    } else {
     205                        loadedDirectories.add(canonical);
     206                    }
     207
     208                    Collection<File> children = Arrays.asList(f.listFiles(JpegFileFilter.getInstance()));
     209                    if (children != null) {
     210                        progressMonitor.subTask(tr("Scanning directory {0}", f.getPath()));
     211                        try {
     212                            addRecursiveFiles(files, children);
     213                        } catch(NullPointerException npe) {
     214                            npe.printStackTrace();
     215                            rememberError(tr("Found null file in directory {0}\n", f.getPath()));
     216                        }
     217                    } else {
     218                        rememberError(tr("Error while getting files from directory {0}\n", f.getPath()));
     219                    }
     220
     221                } else {
     222                      files.add(f);
     223                }
     224            }
     225
     226            if (nullFile) {
     227                throw new NullPointerException();
     228            }
     229        }
     230
     231        protected String formatErrorMessages() {
     232                StringBuffer sb = new StringBuffer();
     233                sb.append("<html>");
     234                if (errorMessages.size() == 1) {
     235                        sb.append(errorMessages.iterator().next());
     236                } else {
     237                        sb.append("<ul>");
     238                        for (String msg: errorMessages) {
     239                                sb.append("<li>").append(msg).append("</li>");
     240                        }
     241                        sb.append("/ul>");
     242                }
     243                sb.append("</html>");
     244                return sb.toString();
     245        }
     246
    350247        @Override protected void finish() {
    351             if (gps.isEmpty()) {
    352                 JOptionPane.showMessageDialog(
    353                         Main.parent,
    354                         tr("No images with readable timestamps found."),
    355                         tr("Warning"),
    356                         JOptionPane.WARNING_MESSAGE
    357                 );
    358                 return;
    359             }
     248                if (!errorMessages.isEmpty()) {
     249                        JOptionPane.showMessageDialog(
     250                                        Main.parent,
     251                                        formatErrorMessages(),
     252                                        tr("Error"),
     253                                        JOptionPane.ERROR_MESSAGE
     254                        );
     255                }
    360256            if (layer != null) {
    361257                Main.main.addLayer(layer);
    362             }
    363         }
    364 
    365         @Override
    366         protected void cancel() {
    367 
    368         }
    369     }
    370 
    371     public ArrayList<ImageEntry> data;
    372     private LinkedList<TimedPoint> gps = new LinkedList<TimedPoint>();
    373 
    374     /**
    375      * The delta added to all timestamps in files from the camera
    376      * to match to the timestamp from the gps receivers tracklog.
    377      */
    378     private long delta = Long.parseLong(Main.pref.get("tagimages.delta", "0"));
    379     private long gpstimezone = Long.parseLong(Main.pref.get("tagimages.gpstimezone", "0"))*60*60*1000;
    380     private boolean mousePressed = false;
    381     private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
    382     private MouseAdapter mouseAdapter;
    383     private ImageViewerDialog imageViewerDialog;
    384 
    385     public static final class GpsTimeIncorrect extends Exception {
    386         public GpsTimeIncorrect(String message, Throwable cause) {
    387             super(message, cause);
    388         }
    389         public GpsTimeIncorrect(String message) {
    390             super(message);
    391         }
    392     }
    393 
    394     private static final class TimedPoint implements Comparable<TimedPoint> {
    395         Date time;
    396         CachedLatLon pos;
    397 
    398         public TimedPoint(Date time, LatLon pos) {
    399             this.time = time;
    400             this.pos = new CachedLatLon(pos);
    401         }
    402         public int compareTo(TimedPoint point) {
    403             return time.compareTo(point.time);
    404         }
    405     }
     258                layer.hook_up_mouse_events(); // Main.map.mapView should exist
     259                                              // now. Can add mouse listener
     260
     261                if (! cancelled && layer.data.size() > 0) {
     262                    boolean noGeotagFound = true;
     263                    for (ImageEntry e : layer.data) {
     264                        if (e.pos != null) {
     265                            noGeotagFound = false;
     266                        }
     267                    }
     268                    if (noGeotagFound) {
     269                        new CorrelateGpxWithImages(layer).actionPerformed(null);
     270                    }
     271                }
     272            }
     273        }
     274
     275        @Override protected void cancel() {
     276            cancelled = true;
     277        }
     278       
     279        class Thumbsloader implements Runnable {
     280            public void run() {
     281                System.err.println("Load Thumbnails");
     282                MediaTracker tracker = new MediaTracker(Main.map.mapView);
     283                for (int i = 0; i < layer.data.size(); i++) {
     284                    System.err.println("getImg "+i);
     285                    Image img = Toolkit.getDefaultToolkit().createImage(layer.data.get(i).file.getPath());
     286                    tracker.addImage(img, 0);
     287                    try {
     288                                tracker.waitForID(0);
     289                    } catch (InterruptedException e) {
     290                        System.err.println("InterruptedException");
     291                                return; //  FIXME
     292                    }
     293                    BufferedImage scaledBI = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
     294                    Graphics2D g = scaledBI.createGraphics();
     295                    while (!g.drawImage(img, 0, 0, 16, 16, null))
     296                    {
     297                        try {
     298                            Thread.sleep(10);
     299                        } catch(InterruptedException ie) {}
     300                    }
     301                    g.dispose();
     302                    tracker.removeImage(img);
     303                    layer.data.get(i).thumbnail = scaledBI;
     304                    if (Main.map != null && Main.map.mapView != null) {
     305                        Main.map.mapView.repaint();
     306                    }
     307                }
     308               
     309//                boolean error = tracker.isErrorID(1);
     310//                if (img != null && (img.getWidth(null) == 0 || img.getHeight(null) == 0)) {
     311//                    error = true;
     312//                }
     313
     314
     315            }
     316        }       
     317    }
     318   
     319    private static boolean addedToggleDialog = false;
    406320
    407321    public static void create(Collection<File> files, GpxLayer gpxLayer) {
    408322        Loader loader = new Loader(files, gpxLayer);
    409323        Main.worker.execute(loader);
    410     }
    411 
    412     private GeoImageLayer(final ArrayList<ImageEntry> data, LinkedList<TimedPoint> gps) {
     324        if (!addedToggleDialog) {
     325            Main.map.addToggleDialog(ImageViewerDialog.getInstance());
     326            addedToggleDialog = true;
     327        }
     328    }
     329
     330    private GeoImageLayer(final List<ImageEntry> data) {
     331
    413332        super(tr("Geotagged Images"));
     333
    414334        Collections.sort(data);
    415         Collections.sort(gps);
    416335        this.data = data;
    417         this.gps = gps;
    418         final Layer self = this;
    419         mouseAdapter = new MouseAdapter(){
     336    }
     337
     338    @Override
     339    public Icon getIcon() {
     340        return ImageProvider.get("dialogs/geoimage");
     341    }
     342
     343    @Override
     344    public Object getInfoComponent() {
     345        // TODO Auto-generated method stub
     346        return null;
     347    }
     348
     349    @Override
     350    public Component[] getMenuEntries() {
     351
     352        JMenuItem correlateItem = new JMenuItem(tr("Correlate to GPX"), ImageProvider.get("dialogs/geoimage/gpx2img"));
     353        correlateItem.addActionListener(new CorrelateGpxWithImages(this));
     354
     355        return new Component[] {
     356                new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
     357                new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)),
     358                new JMenuItem(new RenameLayerAction(null, this)),
     359                new JSeparator(),
     360                correlateItem
     361                };
     362    }
     363
     364    @Override
     365    public String getToolTipText() {
     366        int i = 0;
     367        for (ImageEntry e : data)
     368            if (e.pos != null)
     369                i++;
     370        return data.size() + " " + trn("image", "images", data.size())
     371                + " loaded. " + tr("{0} were found to be gps tagged.", i);
     372    }
     373
     374    @Override
     375    public boolean isMergable(Layer other) {
     376        return other instanceof GeoImageLayer;
     377    }
     378
     379    @Override
     380    public void mergeFrom(Layer from) {
     381        GeoImageLayer l = (GeoImageLayer) from;
     382
     383        ImageEntry selected = null;
     384        if (l.currentPhoto >= 0) {
     385            selected = l.data.get(l.currentPhoto);
     386        }
     387
     388        data.addAll(l.data);
     389        Collections.sort(data);
     390
     391        // Supress the double photos.
     392        if (data.size() > 1) {
     393            ImageEntry cur;
     394            ImageEntry prev = data.get(data.size() - 1);
     395            for (int i = data.size() - 2; i >= 0; i--) {
     396                cur = data.get(i);
     397                if (cur.file.equals(prev.file)) {
     398                    data.remove(i);
     399                } else {
     400                    prev = cur;
     401                }
     402            }
     403        }
     404
     405        if (selected != null) {
     406            for (int i = 0; i < data.size() ; i++) {
     407                if (data.get(i) == selected) {
     408                    currentPhoto = i;
     409                    ImageViewerDialog.showImage(GeoImageLayer.this, data.get(i));
     410                    break;
     411                }
     412            }
     413        }
     414
     415        setName(l.getName());
     416
     417    }
     418
     419    @Override
     420    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
     421
     422        for (ImageEntry e : data) {
     423            if (e.pos != null) {
     424                Point p = mv.getPoint(e.pos);
     425                if (e.thumbnail != null && e.thumbnail.getWidth(null) > 0 && e.thumbnail.getHeight(null) > 0) {
     426                    g.drawImage(e.thumbnail,
     427                                p.x - e.thumbnail.getWidth(null) / 2,
     428                                p.y - e.thumbnail.getHeight(null) / 2, null);
     429                }
     430                else {
     431                icon.paintIcon(mv, g,
     432                               p.x - icon.getIconWidth() / 2,
     433                               p.y - icon.getIconHeight() / 2);
     434                }
     435            }
     436        }
     437
     438        // Draw the selection on top of the other pictures.
     439        if (currentPhoto >= 0 && currentPhoto < data.size()) {
     440            ImageEntry e = data.get(currentPhoto);
     441
     442            if (e.pos != null) {
     443                Point p = mv.getPoint(e.pos);
     444
     445                Rectangle r = new Rectangle(p.x - selectedIcon.getIconWidth() / 2,
     446                                            p.y - selectedIcon.getIconHeight() / 2,
     447                                            selectedIcon.getIconWidth(),
     448                                            selectedIcon.getIconHeight());
     449                selectedIcon.paintIcon(mv, g, r.x, r.y);
     450            }
     451        }
     452    }
     453
     454    @Override
     455    public void visitBoundingBox(BoundingXYVisitor v) {
     456        for (ImageEntry e : data)
     457            v.visit(e.pos);
     458    }
     459
     460    /*
     461     * Extract gps from image exif
     462     *
     463     * If successful, fills in the LatLon and EastNorth attributes of passed in
     464     * image;
     465     */
     466
     467    private static void extractExif(ImageEntry e) {
     468
     469        try {
     470            int deg;
     471            float min, sec;
     472            double lon, lat;
     473
     474            Metadata metadata = JpegMetadataReader.readMetadata(e.file);
     475            Directory dir = metadata.getDirectory(GpsDirectory.class);
     476
     477            // longitude
     478
     479            Rational[] components = dir
     480                    .getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
     481
     482            deg = components[0].intValue();
     483            min = components[1].floatValue();
     484            sec = components[2].floatValue();
     485
     486            lon = (deg + (min / 60) + (sec / 3600));
     487
     488            if (dir.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF).charAt(0) == 'W')
     489                lon = -lon;
     490
     491            // latitude
     492
     493            components = dir.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
     494
     495            deg = components[0].intValue();
     496            min = components[1].floatValue();
     497            sec = components[2].floatValue();
     498
     499            lat = (deg + (min / 60) + (sec / 3600));
     500
     501            if (dir.getString(GpsDirectory.TAG_GPS_LATITUDE_REF).charAt(0) == 'S')
     502                lat = -lat;
     503
     504            // Store values
     505
     506            e.setCoor(new LatLon(lat, lon));
     507            e.exifCoor = e.pos;
     508
     509        } catch (Exception p) {
     510            e.pos = null;
     511        }
     512    }
     513
     514    public void showNextPhoto() {
     515        if (data != null && data.size() > 0) {
     516            currentPhoto++;
     517            if (currentPhoto >= data.size()) {
     518                currentPhoto = data.size() - 1;
     519            }
     520            ImageViewerDialog.showImage(this, data.get(currentPhoto));
     521        } else {
     522            currentPhoto = -1;
     523        }
     524        Main.main.map.repaint();
     525    }
     526
     527    public void showPreviousPhoto() {
     528        if (data != null && data.size() > 0) {
     529            currentPhoto--;
     530            if (currentPhoto < 0) {
     531                currentPhoto = 0;
     532            }
     533            ImageViewerDialog.showImage(this, data.get(currentPhoto));
     534        } else {
     535            currentPhoto = -1;
     536        }
     537        Main.main.map.repaint();
     538    }
     539   
     540    public void checkPreviousNextButtons() {
     541        System.err.println("check: " + currentPhoto);
     542        ImageViewerDialog.setNextEnabled(currentPhoto < data.size() - 1);
     543        ImageViewerDialog.setPreviousEnabled(currentPhoto > 0);
     544    }
     545
     546    public void removeCurrentPhoto() {
     547        if (data != null && data.size() > 0 && currentPhoto >= 0 && currentPhoto < data.size()) {
     548            data.remove(currentPhoto);
     549            if (currentPhoto >= data.size()) {
     550                currentPhoto = data.size() - 1;
     551            }
     552            if (currentPhoto >= 0) {
     553                ImageViewerDialog.showImage(this, data.get(currentPhoto));
     554            } else {
     555                ImageViewerDialog.showImage(this, null);
     556            }
     557        }
     558        Main.main.map.repaint();
     559    }
     560
     561    private MouseAdapter mouseAdapter = null;
     562
     563    private void hook_up_mouse_events() {
     564        mouseAdapter = new MouseAdapter() {
    420565            @Override public void mousePressed(MouseEvent e) {
    421                 if (e.getButton() != MouseEvent.BUTTON1)
     566
     567                if (e.getButton() != MouseEvent.BUTTON1) {
    422568                    return;
    423                 mousePressed  = true;
    424                 if (isVisible()) {
     569                }
     570                if (isVisible())
    425571                    Main.map.mapView.repaint();
    426                 }
    427             }
     572            }
     573
    428574            @Override public void mouseReleased(MouseEvent ev) {
    429                 if (ev.getButton() != MouseEvent.BUTTON1)
     575
     576                if (ev.getButton() != MouseEvent.BUTTON1) {
    430577                    return;
    431                 mousePressed = false;
    432                 if (!isVisible())
     578                }
     579                if (!isVisible()) {
    433580                    return;
    434                 for (int i = data.size(); i > 0; --i) {
    435                     ImageEntry e = data.get(i-1);
    436                     if (e.pos == null) {
     581                }
     582
     583                ImageViewerDialog d = ImageViewerDialog.getInstance();
     584//                System.err.println(d.isDialogShowing());
     585
     586
     587                for (int i = data.size() - 1; i >= 0; --i) {
     588                    ImageEntry e = data.get(i);
     589                    if (e.pos == null)
    437590                        continue;
    438                     }
    439591                    Point p = Main.map.mapView.getPoint(e.pos);
    440                     Rectangle r = new Rectangle(p.x-ICON_SIZE/2, p.y-ICON_SIZE/2, ICON_SIZE, ICON_SIZE);
     592                    Rectangle r = new Rectangle(p.x - icon.getIconWidth() / 2,
     593                                                p.y - icon.getIconHeight() / 2,
     594                                                icon.getIconWidth(),
     595                                                icon.getIconHeight());
    441596                    if (r.contains(ev.getPoint())) {
    442                         showImage(i-1);
     597                        currentPhoto = i;
     598                        ImageViewerDialog.showImage(GeoImageLayer.this, e);
     599                        Main.main.map.repaint();
     600                       
     601                       
    443602                        break;
    444603                    }
     
    448607        };
    449608        Main.map.mapView.addMouseListener(mouseAdapter);
    450         Layer.listeners.add(new LayerChangeListener(){
    451             public void activeLayerChange(Layer oldLayer, Layer newLayer) {}
    452             public void layerAdded(Layer newLayer) {}
     609        Layer.listeners.add(new LayerChangeListener() {
     610            public void activeLayerChange(Layer oldLayer, Layer newLayer) {
     611                if (newLayer == GeoImageLayer.this && currentPhoto >= 0) {
     612                    Main.main.map.repaint();
     613                    ImageViewerDialog.showImage(GeoImageLayer.this, data.get(currentPhoto));
     614                }
     615            }
     616
     617            public void layerAdded(Layer newLayer) {
     618            }
     619
    453620            public void layerRemoved(Layer oldLayer) {
    454                 if (oldLayer == self) {
     621                if (oldLayer == GeoImageLayer.this) {
    455622                    Main.map.mapView.removeMouseListener(mouseAdapter);
     623                    currentPhoto = -1;
     624                    data.clear();
     625                    data = null;
    456626                }
    457627            }
     
    459629    }
    460630
    461     private class ImageViewerDialog {
    462 
    463         private int currentImage;
    464         private ImageEntry currentImageEntry;
    465 
    466         private final JDialog dlg;
    467         private final JButton nextButton;
    468         private final JButton prevButton;
    469         private final JToggleButton scaleToggle;
    470         private final JToggleButton centerToggle;
    471         private final JViewport imageViewport;
    472         private final JLabel imageLabel;
    473 
    474         private class ImageAction implements ActionListener {
    475 
    476             private final int offset;
    477 
    478             public ImageAction(int offset) {
    479                 this.offset = offset;
    480             }
    481 
    482             public void actionPerformed(ActionEvent e) {
    483                 showImage(currentImage + offset);
    484             }
    485 
    486         }
    487 
    488         public ImageViewerDialog(ImageEntry firstImage) {
    489             final JPanel p = new JPanel(new BorderLayout());
    490             imageLabel = new JLabel(new ImageIcon(imageLoader.waitForImage(firstImage.image, 580)));
    491             final JScrollPane scroll = new JScrollPane(imageLabel);
    492             scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
    493             scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    494             imageViewport = scroll.getViewport();
    495             p.add(scroll, BorderLayout.CENTER);
    496 
    497             scaleToggle = new JToggleButton(ImageProvider.get("dialogs", "zoom-best-fit"));
    498             nextButton  = new JButton(ImageProvider.get("dialogs", "next"));
    499             prevButton = new JButton(ImageProvider.get("dialogs", "previous"));
    500             centerToggle = new JToggleButton(ImageProvider.get("dialogs", "centreview"));
    501 
    502             JPanel p2 = new JPanel();
    503             p2.add(prevButton);
    504             p2.add(scaleToggle);
    505             p2.add(centerToggle);
    506             p2.add(nextButton);
    507             p.add(p2, BorderLayout.SOUTH);
    508             final JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE);
    509             dlg = pane.createDialog(Main.parent, "");
    510             scaleToggle.addActionListener(new ImageAction(0));
    511             scaleToggle.setSelected(true);
    512             centerToggle.addActionListener(new ImageAction(0));
    513 
    514             nextButton.setActionCommand("Next");
    515             prevButton.setActionCommand("Previous");
    516             nextButton.setMnemonic(KeyEvent.VK_RIGHT);
    517             prevButton.setMnemonic(KeyEvent.VK_LEFT);
    518             scaleToggle.setMnemonic(KeyEvent.VK_F);
    519             centerToggle.setMnemonic(KeyEvent.VK_C);
    520             nextButton.setToolTipText("Show next image");
    521             prevButton.setToolTipText("Show previous image");
    522             centerToggle.setToolTipText("Centre image location in main display");
    523             scaleToggle.setToolTipText("Scale image to fit");
    524 
    525             prevButton.addActionListener(new ImageAction(-1));
    526             nextButton.addActionListener(new ImageAction(1));
    527             centerToggle.setSelected(false);
    528 
    529             dlg.addComponentListener(new ComponentListener() {
    530                 boolean ignoreEvent = true;
    531                 public void componentHidden(ComponentEvent e) {}
    532                 public void componentMoved(ComponentEvent e) {}
    533                 public void componentResized(ComponentEvent ev) {
    534                     // we ignore the first resize event, as the picture is scaled already on load:
    535                     if (scaleToggle.getModel().isSelected() && !ignoreEvent) {
    536                         imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image,
    537                                 Math.max(imageViewport.getWidth(), imageViewport.getHeight()))));
    538                     }
    539                     ignoreEvent = false;
    540                 }
    541                 public void componentShown(ComponentEvent e) {}
    542 
    543             });
    544             dlg.setModal(false);
    545             dlg.setResizable(true);
    546             dlg.pack();
    547         }
    548 
    549         public void showImage(int index) {
    550             dlg.setVisible(true);
    551             dlg.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    552 
    553             if (index < 0) {
    554                 index = 0;
    555             } else if (index >= data.size() - 1) {
    556                 index = data.size() - 1;
    557             }
    558 
    559             currentImage = index;
    560             currentImageEntry = data.get(currentImage);
    561 
    562             prevButton.setEnabled(currentImage > 0);
    563             nextButton.setEnabled(currentImage < data.size() - 1);
    564 
    565             if (scaleToggle.getModel().isSelected()) {
    566                 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image,
    567                         Math.max(imageViewport.getWidth(), imageViewport.getHeight()))));
    568             } else {
    569                 imageLabel.setIcon(new ImageIcon(imageLoader.waitForImage(currentImageEntry.image)));
    570             }
    571 
    572             if (centerToggle.getModel().isSelected()) {
    573                 Main.map.mapView.zoomTo(currentImageEntry.pos);
    574             }
    575 
    576             dlg.setTitle(currentImageEntry.image +
    577                     " (" + currentImageEntry.pos.toDisplayString() + ")");
    578             dlg.setCursor(Cursor.getDefaultCursor());
    579         }
    580 
    581     }
    582 
    583     private void showImage(int i) {
    584         if (imageViewerDialog == null) {
    585             imageViewerDialog = new ImageViewerDialog(data.get(i));
    586         }
    587         imageViewerDialog.showImage(i);
    588     }
    589 
    590     @Override public Icon getIcon() {
    591         return ImageProvider.get("layer", "tagimages_small");
    592     }
    593 
    594     @Override public Object getInfoComponent() {
    595         JPanel p = new JPanel(new GridBagLayout());
    596         p.add(new JLabel(getToolTipText()), GBC.eop());
    597 
    598         p.add(new JLabel(tr("GPS start: {0}",dateFormat.format(gps.getFirst().time))), GBC.eol());
    599         p.add(new JLabel(tr("GPS end: {0}",dateFormat.format(gps.getLast().time))), GBC.eop());
    600 
    601         p.add(new JLabel(tr("current delta: {0}s",(delta/1000.0))), GBC.eol());
    602         p.add(new JLabel(tr("timezone difference: ")+(gpstimezone>0?"+":"")+(gpstimezone/1000/60/60)), GBC.eop());
    603 
    604         JList img = new JList(data.toArray());
    605         img.setCellRenderer(new DefaultListCellRenderer(){
    606             @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
    607                 super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
    608                 ImageEntry e = (ImageEntry)value;
    609                 setIcon(new ImageIcon(e.getIcon()));
    610                 setText(e.image.getName()+" ("+dateFormat.format(new Date(e.time.getTime()+(delta+gpstimezone)))+")");
    611                 if (e.pos == null) {
    612                     setForeground(Color.red);
    613                 }
    614                 return this;
    615             }
    616         });
    617         img.setVisibleRowCount(5);
    618         p.add(new JScrollPane(img), GBC.eop().fill(GBC.BOTH));
    619         return p;
    620     }
    621 
    622     @Override public String getToolTipText() {
    623         int i = 0;
    624         for (ImageEntry e : data)
    625             if (e.pos != null) {
    626                 i++;
    627             }
    628         return data.size()+" "+trn("image","images",data.size())+". "+tr("{0} within the track.",i);
    629     }
    630 
    631     @Override public boolean isMergable(Layer other) {
    632         return other instanceof GeoImageLayer;
    633     }
    634 
    635     @Override public void mergeFrom(Layer from) {
    636         GeoImageLayer l = (GeoImageLayer)from;
    637         data.addAll(l.data);
    638     }
    639 
    640     @Override public void paint(Graphics2D g, MapView mv, Bounds box) {
    641         int clickedIndex = -1;
    642 
    643         // First select beveled icon (for cases where are more icons on the same spot)
    644         Point mousePosition = mv.getMousePosition();
    645         if (mousePosition != null  && mousePressed) {
    646             for (int i = data.size() - 1; i >= 0; i--) {
    647                 ImageEntry e = data.get(i);
    648                 if (e.pos == null) {
    649                     continue;
    650                 }
    651 
    652                 Point p = mv.getPoint(e.pos);
    653                 Rectangle r = new Rectangle(p.x-ICON_SIZE / 2, p.y-ICON_SIZE / 2, ICON_SIZE, ICON_SIZE);
    654                 if (r.contains(mousePosition)) {
    655                     clickedIndex = i;
    656                     break;
    657                 }
    658             }
    659         }
    660 
    661         for (int i = 0; i < data.size(); i++) {
    662             ImageEntry e = data.get(i);
    663             if (e.pos != null) {
    664                 Point p = mv.getPoint(e.pos);
    665                 Rectangle r = new Rectangle(p.x-ICON_SIZE / 2, p.y-ICON_SIZE / 2, ICON_SIZE, ICON_SIZE);
    666                 g.drawImage(e.getIcon(), r.x, r.y, null);
    667                 Border b = null;
    668                 if (i == clickedIndex) {
    669                     b = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
    670                 } else {
    671                     b = BorderFactory.createBevelBorder(BevelBorder.RAISED);
    672                 }
    673                 Insets inset = b.getBorderInsets(mv);
    674                 r.grow((inset.top+inset.bottom)/2, (inset.left+inset.right)/2);
    675                 b.paintBorder(mv, g, r.x, r.y, r.width, r.height);
    676             }
    677         }
    678     }
    679 
    680     @Override public void visitBoundingBox(BoundingXYVisitor v) {
    681         for (ImageEntry e : data) {
    682             v.visit(e.pos);
    683         }
    684     }
    685 
    686     @Override public Component[] getMenuEntries() {
    687         JMenuItem sync = new JMenuItem(tr("Sync clock"), ImageProvider.get("clock"));
    688         sync.addActionListener(new ActionListener(){
    689             public void actionPerformed(ActionEvent e) {
    690                 JFileChooser fc = new JFileChooser(Main.pref.get("tagimages.lastdirectory"));
    691                 fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
    692                 fc.setAcceptAllFileFilterUsed(false);
    693                 fc.setFileFilter(new FileFilter(){
    694                     @Override public boolean accept(File f) {
    695                         return f.isDirectory() || f.getName().toLowerCase().endsWith(".jpg");
    696                     }
    697                     @Override public String getDescription() {
    698                         return tr("JPEG images (*.jpg)");
    699                     }
    700                 });
    701                 fc.showOpenDialog(Main.parent);
    702                 File sel = fc.getSelectedFile();
    703                 if (sel == null)
    704                     return;
    705                 Main.pref.put("tagimages.lastdirectory", sel.getPath());
    706                 sync(sel);
    707                 Main.map.repaint();
    708             }
    709         });
    710         return new Component[]{
    711                 new JMenuItem(LayerListDialog.getInstance().createShowHideLayerAction(this)),
    712                 new JMenuItem(LayerListDialog.getInstance().createDeleteLayerAction(this)),
    713                 new JSeparator(),
    714                 sync,
    715                 new JSeparator(),
    716                 new JMenuItem(new RenameLayerAction(null, this)),
    717                 new JSeparator(),
    718                 new JMenuItem(new LayerListPopup.InfoAction(this))};
    719     }
    720 
    721     private void calculatePosition() {
    722         for (ImageEntry e : data) {
    723             TimedPoint lastTP = null;
    724             for (TimedPoint tp : gps) {
    725                 Date time = new Date(tp.time.getTime() - (delta+gpstimezone));
    726                 if (time.after(e.time) && lastTP != null) {
    727                     e.pos = new CachedLatLon(lastTP.pos.getCenter(tp.pos));
    728                     break;
    729                 }
    730                 lastTP = tp;
    731             }
    732             if (e.pos == null) {
    733                 e.pos = gps.getLast().pos;
    734             }
    735         }
    736     }
    737 
    738     private void sync(File f) {
    739         Date exifDate;
    740         try {
    741             exifDate = ExifReader.readTime(f);
    742         } catch (ParseException e) {
    743             JOptionPane.showMessageDialog(
    744                     Main.parent,
    745                     tr("The date in file \"{0}\" could not be parsed.", f.getName()),
    746                     tr("Error"),
    747                     JOptionPane.ERROR_MESSAGE
    748             );
    749             return;
    750         }
    751         if (exifDate == null) {
    752             JOptionPane.showMessageDialog(
    753                     Main.parent,
    754                     tr("There is no EXIF time within the file \"{0}\".", f.getName()),
    755                     tr("Error"),
    756                     JOptionPane.ERROR_MESSAGE
    757             );
    758             return;
    759         }
    760         JPanel p = new JPanel(new GridBagLayout());
    761         p.add(new JLabel(tr("Image")), GBC.eol());
    762         p.add(new JLabel(new ImageIcon(imageLoader.waitForImage(f, 300))), GBC.eop());
    763         p.add(new JLabel(tr("Enter shown date (mm/dd/yyyy HH:MM:SS)")), GBC.eol());
    764         JTextField gpsText = new JTextField(dateFormat.format(new Date(exifDate.getTime()+delta)));
    765         p.add(gpsText, GBC.eol().fill(GBC.HORIZONTAL));
    766         p.add(new JLabel(tr("GPS unit timezone (difference to photo)")), GBC.eol());
    767         String t = Main.pref.get("tagimages.gpstimezone", "0");
    768         if (t.charAt(0) != '-') {
    769             t = "+"+t;
    770         }
    771         JTextField gpsTimezone = new JTextField(t);
    772         p.add(gpsTimezone, GBC.eol().fill(GBC.HORIZONTAL));
    773 
    774         while (true) {
    775             int answer = JOptionPane.showConfirmDialog(
    776                     Main.parent,
    777                     p,
    778                     tr("Synchronize Time with GPS Unit"),
    779                     JOptionPane.OK_CANCEL_OPTION,
    780                     JOptionPane.QUESTION_MESSAGE
    781             );
    782             if (answer != JOptionPane.OK_OPTION || gpsText.getText().equals(""))
    783                 return;
    784             try {
    785                 delta = DateParser.parse(gpsText.getText()).getTime() - exifDate.getTime();
    786                 String time = gpsTimezone.getText();
    787                 if (!time.equals("") && time.charAt(0) == '+') {
    788                     time = time.substring(1);
    789                 }
    790                 if (time.equals("")) {
    791                     time = "0";
    792                 }
    793                 gpstimezone = Long.valueOf(time)*60*60*1000;
    794                 Main.pref.put("tagimages.delta", ""+delta);
    795                 Main.pref.put("tagimages.gpstimezone", time);
    796                 calculatePosition();
    797                 return;
    798             } catch (NumberFormatException x) {
    799                 JOptionPane.showMessageDialog(
    800                         Main.parent,
    801                         tr("Time entered could not be parsed."),
    802                         tr("Error"),
    803                         JOptionPane.ERROR_MESSAGE
    804                 );
    805             } catch (ParseException x) {
    806                 JOptionPane.showMessageDialog(
    807                         Main.parent,
    808                         tr("Time entered could not be parsed."),
    809                         tr("Error"),
    810                         JOptionPane.ERROR_MESSAGE
    811                 );
    812             }
    813         }
    814     }
    815 
    816631}
Note: See TracChangeset for help on using the changeset viewer.