Ticket #17050: refactor_geoimagelayer.patch

File refactor_geoimagelayer.patch, 55.4 KB (added by francois2, 3 months ago)
  • new file src/org/openstreetmap/josm/data/ImageData.java

    diff --git a/src/org/openstreetmap/josm/data/ImageData.java b/src/org/openstreetmap/josm/data/ImageData.java
    new file mode 100644
    index 000000000..0d01699db
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data;
     3
     4import java.util.ArrayList;
     5import java.util.Collections;
     6import java.util.List;
     7
     8import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
     9import org.openstreetmap.josm.tools.ListenerList;
     10
     11/**
     12 * Class to hold {@link ImageEntry} and the current selection
     13 * @since xxx
     14 */
     15public class ImageData {
     16    /**
     17     * A listener that is informed when the current selection change
     18     */
     19    public interface ImageDataUpdateListener {
     20        /**
     21         * Called when the selection change
     22         * @param data the image data
     23         */
     24        void selectedImageChanged(ImageData data);
     25    }
     26
     27    private final List<ImageEntry> data;
     28
     29    private int selectedImageIndex = -1;
     30
     31    private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create();
     32
     33    /**
     34     * Construct a new image container without images
     35     */
     36    public ImageData() {
     37        this(null);
     38    }
     39
     40    /**
     41     * Construct a new image container with a list of images
     42     * @param data the list of {@link ImageEntry}
     43     */
     44    public ImageData(List<ImageEntry> data) {
     45        if (data != null) {
     46            Collections.sort(data);
     47            this.data = data;
     48        } else {
     49            this.data = new ArrayList<>();
     50        }
     51    }
     52
     53    /**
     54     * Returns the images
     55     * @return the images
     56     */
     57    public List<ImageEntry> getImages() {
     58        return this.data;
     59    }
     60
     61    /**
     62     * Determines if one image has modified GPS data.
     63     * @return {@code true} if data has been modified; {@code false}, otherwise
     64     */
     65    public boolean isModified() {
     66        for (ImageEntry e : data) {
     67            if (e.hasNewGpsData()) {
     68                return true;
     69            }
     70        }
     71        return false;
     72    }
     73
     74    /**
     75     * Merge 2 ImageData
     76     * @param data {@link ImageData}
     77     */
     78    public void mergeFrom(ImageData data) {
     79        this.data.addAll(data.getImages());
     80        Collections.sort(this.data);
     81
     82        final ImageEntry selected = data.getSelectedImage();
     83
     84        // Suppress the double photos.
     85        if (this.data.size() > 1) {
     86            ImageEntry cur;
     87            ImageEntry prev = this.data.get(this.data.size() - 1);
     88            for (int i = this.data.size() - 2; i >= 0; i--) {
     89                cur = this.data.get(i);
     90                if (cur.getFile().equals(prev.getFile())) {
     91                    this.data.remove(i);
     92                } else {
     93                    prev = cur;
     94                }
     95            }
     96        }
     97        if (selected != null) {
     98            this.setSelectedImageIndex(this.data.indexOf(selected));
     99        }
     100    }
     101
     102    /**
     103     * Return the current selected image
     104     * @return the selected image as {@link ImageEntry} or null
     105     */
     106    public ImageEntry getSelectedImage() {
     107        if (this.selectedImageIndex > -1) {
     108            return data.get(this.selectedImageIndex);
     109        }
     110        return null;
     111    }
     112
     113    /**
     114     * Select the first image of the sequence
     115     */
     116    public void selectFirstImage() {
     117        if (!data.isEmpty()) {
     118            this.setSelectedImageIndex(0);
     119        }
     120    }
     121
     122    /**
     123     * Select the last image of the sequence
     124     */
     125    public void selectLastImage() {
     126        this.setSelectedImageIndex(data.size() - 1);
     127    }
     128
     129    /**
     130     * Check if there is a next image in the sequence
     131     * @return {@code true} is there is a next image, {@code false} otherwise
     132     */
     133    public boolean hasNextImage() {
     134        return (this.selectedImageIndex != data.size() - 1);
     135    }
     136
     137    /**
     138     * Select the next image of the sequence
     139     */
     140    public void selectNextImage() {
     141        if (this.hasNextImage()) {
     142            this.setSelectedImageIndex(this.selectedImageIndex + 1);
     143        }
     144    }
     145
     146    /**
     147     *  Check if there is a previous image in the sequence
     148     * @return {@code true} is there is a previous image, {@code false} otherwise
     149     */
     150    public boolean hasPreviousImage() {
     151        return this.selectedImageIndex - 1 > -1;
     152    }
     153
     154    /**
     155     * Select the previous image of the sequence
     156     */
     157    public void selectPreviousImage() {
     158        if (data.size() == 0) {
     159            return;
     160        }
     161        this.setSelectedImageIndex(Integer.max(0, this.selectedImageIndex - 1));
     162    }
     163
     164    /**
     165     * Select as the selected the given image
     166     * @param image
     167     */
     168    public void setSelectedImage(ImageEntry image) {
     169        this.setSelectedImageIndex(this.data.indexOf(image));
     170    }
     171
     172    /**
     173     * Clear the selected image
     174     */
     175    public void clearSelectedImage() {
     176        this.setSelectedImageIndex(-1);
     177    }
     178
     179    private void setSelectedImageIndex(int index) {
     180        this.setSelectedImageIndex(index, false);
     181    }
     182
     183    private void setSelectedImageIndex(int index, boolean forceTrigger) {
     184        if (index == this.selectedImageIndex && !forceTrigger) {
     185            return;
     186        }
     187        this.selectedImageIndex = index;
     188        listeners.fireEvent(l -> l.selectedImageChanged(this));
     189    }
     190
     191    /**
     192     * Remove the current selected image from the list
     193     */
     194    public void removeSelectedImage() {
     195        data.remove(this.getSelectedImage());
     196        if (this.selectedImageIndex == data.size()) {
     197            this.setSelectedImageIndex(data.size() - 1);
     198        } else {
     199            this.setSelectedImageIndex(this.selectedImageIndex, true);
     200        }
     201    }
     202
     203    /**
     204     * Add a listener that listens to image data changes
     205     * @param listener
     206     */
     207    public void addImageDataUpdateListener(ImageDataUpdateListener listener) {
     208        listeners.addListener(listener);
     209    }
     210
     211    /**
     212     * Removes a listener that listens to image data changes
     213     * @param listener The listener
     214     */
     215    public void removeImageDataUpdateListener(ImageDataUpdateListener listener) {
     216        listeners.removeListener(listener);
     217    }
     218}
  • src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java b/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java
    index 9badaffd1..2488f409c 100644
    a b public class CorrelateGpxWithImages extends AbstractAction { 
    178178                break;
    179179            case CANCEL:
    180180                if (yLayer != null) {
    181                     if (yLayer.data != null) {
    182                         for (ImageEntry ie : yLayer.data) {
    183                             ie.discardTmp();
    184                         }
     181                    for (ImageEntry ie : yLayer.getImageData().getImages()) {
     182                        ie.discardTmp();
    185183                    }
     184
    186185                    yLayer.updateBufferAndRepaint();
    187186                }
    188187                break;
    public class CorrelateGpxWithImages extends AbstractAction { 
    216215                    MainApplication.getMap().mapView.zoomTo(bbox);
    217216                }
    218217
    219                 if (yLayer.data != null) {
    220                     for (ImageEntry ie : yLayer.data) {
    221                         ie.applyTmp();
    222                     }
     218
     219                for (ImageEntry ie : yLayer.getImageData().getImages()) {
     220                    ie.applyTmp();
    223221                }
    224222
     223
    225224                yLayer.updateBufferAndRepaint();
    226225
    227226                break;
    public class CorrelateGpxWithImages extends AbstractAction { 
    645644            JList<String> imgList = new JList<>(new AbstractListModel<String>() {
    646645                @Override
    647646                public String getElementAt(int i) {
    648                     return yLayer.data.get(i).getFile().getName();
     647                    return yLayer.getImageData().getImages().get(i).getFile().getName();
    649648                }
    650649
    651650                @Override
    652651                public int getSize() {
    653                     return yLayer.data != null ? yLayer.data.size() : 0;
     652                    return yLayer.getImageData().getImages().size();
    654653                }
    655654            });
    656655            imgList.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    657656            imgList.getSelectionModel().addListSelectionListener(evt -> {
    658657                int index = imgList.getSelectedIndex();
    659                 imgDisp.setImage(yLayer.data.get(index));
    660                 Date date = yLayer.data.get(index).getExifTime();
     658                ImageEntry img = yLayer.getImageData().getImages().get(index);
     659                imgDisp.setImage(img);
     660                Date date = img.getExifTime();
    661661                if (date != null) {
    662662                    DateFormat df = DateUtils.getDateTimeFormat(DateFormat.SHORT, DateFormat.MEDIUM);
    663663                    lbExifTime.setText(df.format(date));
    public class CorrelateGpxWithImages extends AbstractAction { 
    10411041
    10421042            // The selection of images we are about to correlate may have changed.
    10431043            // So reset all images.
    1044             if (yLayer.data != null) {
    1045                 for (ImageEntry ie: yLayer.data) {
    1046                     ie.discardTmp();
    1047                 }
     1044            for (ImageEntry ie: yLayer.getImageData().getImages()) {
     1045                ie.discardTmp();
    10481046            }
    10491047
    10501048            // Construct a list of images that have a date, and sort them on the date.
    public class CorrelateGpxWithImages extends AbstractAction { 
    13001298     * @return matching images
    13011299     */
    13021300    private List<ImageEntry> getSortedImgList(boolean exif, boolean tagged) {
    1303         if (yLayer.data == null) {
    1304             return Collections.emptyList();
    1305         }
    1306         List<ImageEntry> dateImgLst = new ArrayList<>(yLayer.data.size());
    1307         for (ImageEntry e : yLayer.data) {
     1301        List<ImageEntry> dateImgLst = new ArrayList<>(yLayer.getImageData().getImages().size());
     1302        for (ImageEntry e : yLayer.getImageData().getImages()) {
    13081303            if (!e.hasExifTime()) {
    13091304                continue;
    13101305            }
  • src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
    index efbdac4fd..faa65406b 100644
    a b import java.io.IOException; 
    2323import java.util.ArrayList;
    2424import java.util.Arrays;
    2525import java.util.Collection;
    26 import java.util.Collections;
    2726import java.util.HashSet;
    2827import java.util.LinkedHashSet;
    2928import java.util.LinkedList;
    import java.util.concurrent.Executors; 
    3433
    3534import javax.swing.Action;
    3635import javax.swing.Icon;
    37 import javax.swing.JLabel;
    3836import javax.swing.JOptionPane;
    39 import javax.swing.SwingConstants;
    4037
    4138import org.openstreetmap.josm.actions.LassoModeAction;
    4239import org.openstreetmap.josm.actions.RenameLayerAction;
    4340import org.openstreetmap.josm.actions.mapmode.MapMode;
    4441import org.openstreetmap.josm.actions.mapmode.SelectAction;
    4542import org.openstreetmap.josm.data.Bounds;
     43import org.openstreetmap.josm.data.ImageData;
     44import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
    4645import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    47 import org.openstreetmap.josm.gui.ExtendedDialog;
    4846import org.openstreetmap.josm.gui.MainApplication;
    4947import org.openstreetmap.josm.gui.MapFrame;
    5048import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
    5149import org.openstreetmap.josm.gui.MapView;
    5250import org.openstreetmap.josm.gui.NavigatableComponent;
    5351import org.openstreetmap.josm.gui.PleaseWaitRunnable;
    54 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    5552import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    5653import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    5754import org.openstreetmap.josm.gui.io.importexport.JpgImporter;
    import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker; 
    6259import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
    6360import org.openstreetmap.josm.gui.layer.Layer;
    6461import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
    65 import org.openstreetmap.josm.gui.util.GuiHelper;
    6662import org.openstreetmap.josm.tools.ImageProvider;
    6763import org.openstreetmap.josm.tools.Logging;
    6864import org.openstreetmap.josm.tools.Utils;
    import org.openstreetmap.josm.tools.Utils; 
    7167 * Layer displaying geottaged pictures.
    7268 */
    7369public class GeoImageLayer extends AbstractModifiableLayer implements
    74         JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener {
     70        JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener {
    7571
    7672    private static List<Action> menuAdditions = new LinkedList<>();
    7773
    7874    private static volatile List<MapMode> supportedMapModes;
    7975
    80     List<ImageEntry> data;
     76    private final ImageData data;
    8177    GpxLayer gpxLayer;
    8278
    8379    private final Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
    8480    private final Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
    8581
    86     private int currentPhoto = -1;
    87 
    8882    boolean useThumbs;
    8983    private final ExecutorService thumbsLoaderExecutor =
    9084            Executors.newSingleThreadExecutor(Utils.newThreadFactory("thumbnail-loader-%d", Thread.MIN_PRIORITY));
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    151145     */
    152146    public GeoImageLayer(final List<ImageEntry> data, GpxLayer gpxLayer, final String name, boolean useThumbs) {
    153147        super(name != null ? name : tr("Geotagged Images"));
    154         if (data != null) {
    155             Collections.sort(data);
    156         }
    157         this.data = data;
     148        this.data = new ImageData(data);
    158149        this.gpxLayer = gpxLayer;
    159150        this.useThumbs = useThumbs;
     151        this.data.addImageDataUpdateListener(this);
    160152    }
    161153
    162154    /**
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    218210                e.extractExif();
    219211                entries.add(e);
    220212            }
     213
    221214            layer = new GeoImageLayer(entries, gpxLayer);
    222215            files.clear();
    223216        }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    292285            if (layer != null) {
    293286                MainApplication.getLayerManager().addLayer(layer);
    294287
    295                 if (!canceled && layer.data != null && !layer.data.isEmpty()) {
     288                if (!canceled && !layer.getImageData().getImages().isEmpty()) {
    296289                    boolean noGeotagFound = true;
    297                     for (ImageEntry e : layer.data) {
     290                    for (ImageEntry e : layer.getImageData().getImages()) {
    298291                        if (e.getPos() != null) {
    299292                            noGeotagFound = false;
    300293                        }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    356349    private String infoText() {
    357350        int tagged = 0;
    358351        int newdata = 0;
    359         int n = 0;
    360         if (data != null) {
    361             n = data.size();
    362             for (ImageEntry e : data) {
    363                 if (e.getPos() != null) {
    364                     tagged++;
    365                 }
    366                 if (e.hasNewGpsData()) {
    367                     newdata++;
    368                 }
     352        int n = data.getImages().size();
     353        for (ImageEntry e : data.getImages()) {
     354            if (e.getPos() != null) {
     355                tagged++;
     356            }
     357            if (e.hasNewGpsData()) {
     358                newdata++;
    369359            }
    370360        }
    371361        return "<html>"
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    391381     */
    392382    @Override
    393383    public boolean isModified() {
    394         if (data != null) {
    395             for (ImageEntry e : data) {
    396                 if (e.hasNewGpsData()) {
    397                     return true;
    398                 }
    399             }
    400         }
    401         return false;
     384        return this.data.isModified();
    402385    }
    403386
    404387    @Override
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    417400        stopLoadThumbs();
    418401        l.stopLoadThumbs();
    419402
    420         final ImageEntry selected = l.data != null && l.currentPhoto >= 0 ? l.data.get(l.currentPhoto) : null;
    421 
    422         if (l.data != null) {
    423             data.addAll(l.data);
    424         }
    425         Collections.sort(data);
    426 
    427         // Suppress the double photos.
    428         if (data.size() > 1) {
    429             ImageEntry cur;
    430             ImageEntry prev = data.get(data.size() - 1);
    431             for (int i = data.size() - 2; i >= 0; i--) {
    432                 cur = data.get(i);
    433                 if (cur.getFile().equals(prev.getFile())) {
    434                     data.remove(i);
    435                 } else {
    436                     prev = cur;
    437                 }
    438             }
    439         }
    440 
    441         if (selected != null && !data.isEmpty()) {
    442             GuiHelper.runInEDTAndWait(() -> {
    443                 for (int i = 0; i < data.size(); i++) {
    444                     if (selected.equals(data.get(i))) {
    445                         currentPhoto = i;
    446                         ImageViewerDialog.showImage(this, data.get(i));
    447                         break;
    448                     }
    449                 }
    450             });
    451         }
     403        this.data.mergeFrom(l.getImageData());
    452404
    453405        setName(l.getName());
    454406        thumbsLoaded &= l.thumbsLoaded;
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    530482                tempG.fillRect(0, 0, width, height);
    531483                tempG.setComposite(saveComp);
    532484
    533                 if (data != null) {
    534                     for (ImageEntry e : data) {
    535                         paintImage(e, mv, clip, tempG);
    536                     }
    537                     if (currentPhoto >= 0 && currentPhoto < data.size()) {
    538                         // Make sure the selected image is on top in case multiple images overlap.
    539                         paintImage(data.get(currentPhoto), mv, clip, tempG);
    540                     }
     485                for (ImageEntry e : this.data.getImages()) {
     486                    paintImage(e, mv, clip, tempG);
     487                }
     488                if (this.data.getSelectedImage() != null) {
     489                    // Make sure the selected image is on top in case multiple images overlap.
     490                    paintImage(this.data.getSelectedImage(), mv, clip, tempG);
    541491                }
    542492                updateOffscreenBuffer = false;
    543493            }
    544494            g.drawImage(offscreenBuffer, 0, 0, null);
    545         } else if (data != null) {
    546             for (ImageEntry e : data) {
     495        } else {
     496            for (ImageEntry e : data.getImages()) {
    547497                if (e.getPos() == null) {
    548498                    continue;
    549499                }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    554504            }
    555505        }
    556506
    557         if (currentPhoto >= 0 && currentPhoto < data.size()) {
    558             ImageEntry e = data.get(currentPhoto);
    559 
     507        ImageEntry e = data.getSelectedImage();
     508        if (e != null) {
    560509            if (e.getPos() != null) {
    561510                Point p = mv.getPoint(e.getPos());
    562511
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    621570
    622571    @Override
    623572    public void visitBoundingBox(BoundingXYVisitor v) {
    624         for (ImageEntry e : data) {
     573        for (ImageEntry e : data.getImages()) {
    625574            v.visit(e.getPos());
    626575        }
    627576    }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    630579     * Show current photo on map and in image viewer.
    631580     */
    632581    public void showCurrentPhoto() {
    633         clearOtherCurrentPhotos();
    634         if (currentPhoto >= 0) {
    635             ImageViewerDialog.showImage(this, data.get(currentPhoto));
    636         } else {
    637             ImageViewerDialog.showImage(this, null);
     582        if (data.getSelectedImage() != null) {
     583            clearOtherCurrentPhotos();
    638584        }
    639585        updateBufferAndRepaint();
    640586    }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    643589     * Shows next photo.
    644590     */
    645591    public void showNextPhoto() {
    646         if (data != null && !data.isEmpty()) {
    647             currentPhoto++;
    648             if (currentPhoto >= data.size()) {
    649                 currentPhoto = data.size() - 1;
    650             }
    651         } else {
    652             currentPhoto = -1;
    653         }
    654         showCurrentPhoto();
     592        this.data.selectNextImage();
    655593    }
    656594
    657595    /**
    658596     * Shows previous photo.
    659597     */
    660598    public void showPreviousPhoto() {
    661         if (data != null && !data.isEmpty()) {
    662             currentPhoto--;
    663             if (currentPhoto < 0) {
    664                 currentPhoto = 0;
    665             }
    666         } else {
    667             currentPhoto = -1;
    668         }
    669         showCurrentPhoto();
     599        this.data.selectPreviousImage();
    670600    }
    671601
    672602    /**
    673603     * Shows first photo.
    674604     */
    675605    public void showFirstPhoto() {
    676         if (data != null && !data.isEmpty()) {
    677             currentPhoto = 0;
    678         } else {
    679             currentPhoto = -1;
    680         }
    681         showCurrentPhoto();
     606        this.data.selectFirstImage();
    682607    }
    683608
    684609    /**
    685610     * Shows last photo.
    686611     */
    687612    public void showLastPhoto() {
    688         if (data != null && !data.isEmpty()) {
    689             currentPhoto = data.size() - 1;
    690         } else {
    691             currentPhoto = -1;
    692         }
    693         showCurrentPhoto();
    694     }
    695 
    696     public void checkPreviousNextButtons() {
    697         ImageViewerDialog.setNextEnabled(data != null && currentPhoto < data.size() - 1);
    698         ImageViewerDialog.setPreviousEnabled(currentPhoto > 0);
     613        this.data.selectLastImage();
    699614    }
    700615
    701     public void removeCurrentPhoto() {
    702         if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
    703             data.remove(currentPhoto);
    704             if (currentPhoto >= data.size()) {
    705                 currentPhoto = data.size() - 1;
    706             }
    707             showCurrentPhoto();
    708         }
    709     }
    710 
    711     public void removeCurrentPhotoFromDisk() {
    712         ImageEntry toDelete;
    713         if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
    714             toDelete = data.get(currentPhoto);
    715 
    716             int result = new ExtendedDialog(
    717                     MainApplication.getMainFrame(),
    718                     tr("Delete image file from disk"),
    719                     tr("Cancel"), tr("Delete"))
    720             .setButtonIcons("cancel", "dialogs/delete")
    721             .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>",
    722                     toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"), SwingConstants.LEFT))
    723                     .toggleEnable("geoimage.deleteimagefromdisk")
    724                     .setCancelButton(1)
    725                     .setDefaultButton(2)
    726                     .showDialog()
    727                     .getValue();
    728 
    729             if (result == 2) {
    730                 data.remove(currentPhoto);
    731                 if (currentPhoto >= data.size()) {
    732                     currentPhoto = data.size() - 1;
    733                 }
    734 
    735                 if (Utils.deleteFile(toDelete.getFile())) {
    736                     Logging.info("File "+toDelete.getFile()+" deleted. ");
    737                 } else {
    738                     JOptionPane.showMessageDialog(
    739                             MainApplication.getMainFrame(),
    740                             tr("Image file could not be deleted."),
    741                             tr("Error"),
    742                             JOptionPane.ERROR_MESSAGE
    743                             );
    744                 }
    745 
    746                 showCurrentPhoto();
    747             }
    748         }
    749     }
    750 
    751     public void copyCurrentPhotoPath() {
    752         if (data != null && !data.isEmpty() && currentPhoto >= 0 && currentPhoto < data.size()) {
    753             ClipboardUtils.copyString(data.get(currentPhoto).getFile().toString());
    754         }
    755     }
    756 
    757     /**
    758      * Removes a photo from the list of images by index.
    759      * @param idx Image index
    760      * @since 6392
    761      */
    762     public void removePhotoByIdx(int idx) {
    763         if (idx >= 0 && data != null && idx < data.size()) {
    764             data.remove(idx);
    765         }
    766     }
    767616
    768617    /**
    769618     * Check if the position of the mouse event is within the rectangle of the photo icon or thumbnail.
    770      * @param idx Image index, range 0 .. size-1
     619     * @param idx the image index
    771620     * @param evt Mouse event
    772621     * @return {@code true} if the photo matches the mouse position, {@code false} otherwise
    773622     */
    774623    private boolean isPhotoIdxUnderMouse(int idx, MouseEvent evt) {
    775         if (idx >= 0 && data != null && idx < data.size()) {
    776             ImageEntry img = data.get(idx);
    777             if (img.getPos() != null) {
    778                 Point imgCenter = MainApplication.getMap().mapView.getPoint(img.getPos());
    779                 Rectangle imgRect;
    780                 if (useThumbs && img.hasThumbnail()) {
    781                     Dimension imgDim = scaledDimension(img.getThumbnail());
    782                     if (imgDim != null) {
    783                         imgRect = new Rectangle(imgCenter.x - imgDim.width / 2,
    784                                                 imgCenter.y - imgDim.height / 2,
    785                                                 imgDim.width, imgDim.height);
    786                     } else {
    787                         imgRect = null;
    788                     }
     624        ImageEntry img = this.data.getImages().get(idx);
     625        if (img.getPos() != null) {
     626            Point imgCenter = MainApplication.getMap().mapView.getPoint(img.getPos());
     627            Rectangle imgRect;
     628            if (useThumbs && img.hasThumbnail()) {
     629                Dimension imgDim = scaledDimension(img.getThumbnail());
     630                if (imgDim != null) {
     631                    imgRect = new Rectangle(imgCenter.x - imgDim.width / 2,
     632                                            imgCenter.y - imgDim.height / 2,
     633                                            imgDim.width, imgDim.height);
    789634                } else {
    790                     imgRect = new Rectangle(imgCenter.x - icon.getIconWidth() / 2,
    791                                             imgCenter.y - icon.getIconHeight() / 2,
    792                                             icon.getIconWidth(), icon.getIconHeight());
    793                 }
    794                 if (imgRect != null && imgRect.contains(evt.getPoint())) {
    795                     return true;
     635                    imgRect = null;
    796636                }
     637            } else {
     638                imgRect = new Rectangle(imgCenter.x - icon.getIconWidth() / 2,
     639                                        imgCenter.y - icon.getIconHeight() / 2,
     640                                        icon.getIconWidth(), icon.getIconHeight());
     641            }
     642            if (imgRect != null && imgRect.contains(evt.getPoint())) {
     643                return true;
    797644            }
    798645        }
    799646        return false;
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    809656     *               or {@code -1} if there is no image at the mouse position
    810657     */
    811658    private int getPhotoIdxUnderMouse(MouseEvent evt, boolean cycle) {
    812         if (data != null) {
    813             if (cycle && currentPhoto >= 0) {
    814                 // Cycle loop is forward as that is the natural order.
    815                 // Loop 1: One after current photo up to last one.
    816                 for (int idx = currentPhoto + 1; idx < data.size(); ++idx) {
    817                     if (isPhotoIdxUnderMouse(idx, evt)) {
    818                         return idx;
    819                     }
    820                 }
    821                 // Loop 2: First photo up to current one.
    822                 for (int idx = 0; idx <= currentPhoto; ++idx) {
    823                     if (isPhotoIdxUnderMouse(idx, evt)) {
    824                         return idx;
    825                     }
     659        ImageEntry selectedImage = this.data.getSelectedImage();
     660        int selectedIndex = this.data.getImages().indexOf(selectedImage);
     661
     662        if (cycle && selectedImage != null) {
     663            // Cycle loop is forward as that is the natural order.
     664            // Loop 1: One after current photo up to last one.
     665            for (int idx = selectedIndex + 1; idx < this.data.getImages().size(); ++idx) {
     666                if (isPhotoIdxUnderMouse(idx, evt)) {
     667                    return idx;
    826668                }
    827             } else {
    828                 // Check for current photo first, i.e. keep it selected if it is under the mouse.
    829                 if (currentPhoto >= 0 && isPhotoIdxUnderMouse(currentPhoto, evt)) {
    830                     return currentPhoto;
     669            }
     670            // Loop 2: First photo up to current one.
     671            for (int idx = 0; idx <= selectedIndex; ++idx) {
     672                if (isPhotoIdxUnderMouse(idx, evt)) {
     673                    return idx;
    831674                }
    832                 // Loop from last to first to prefer topmost image.
    833                 for (int idx = data.size() - 1; idx >= 0; --idx) {
    834                     if (isPhotoIdxUnderMouse(idx, evt)) {
    835                         return idx;
    836                     }
     675            }
     676        } else {
     677            // Check for current photo first, i.e. keep it selected if it is under the mouse.
     678            if (selectedImage != null && isPhotoIdxUnderMouse(selectedIndex, evt)) {
     679                return selectedIndex;
     680            }
     681            // Loop from last to first to prefer topmost image.
     682            for (int idx = this.data.getImages().size() - 1; idx >= 0; --idx) {
     683                if (isPhotoIdxUnderMouse(idx, evt)) {
     684                    return idx;
    837685                }
    838686            }
    839687        }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    861709    public ImageEntry getPhotoUnderMouse(MouseEvent evt) {
    862710        int idx = getPhotoIdxUnderMouse(evt);
    863711        if (idx >= 0) {
    864             return data.get(idx);
     712            return this.data.getImages().get(idx);
    865713        } else {
    866714            return null;
    867715        }
    868716    }
    869717
    870     /**
    871      * Clears the currentPhoto, i.e. remove select marker, and optionally repaint.
    872      * @param repaint Repaint flag
    873      * @since 6392
    874      */
    875     public void clearCurrentPhoto(boolean repaint) {
    876         currentPhoto = -1;
    877         if (repaint) {
    878             updateBufferAndRepaint();
    879         }
    880     }
    881 
    882718    /**
    883719     * Clears the currentPhoto of the other GeoImageLayer's. Otherwise there could be multiple selected photos.
    884720     */
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    886722        for (GeoImageLayer layer:
    887723                 MainApplication.getLayerManager().getLayersOfType(GeoImageLayer.class)) {
    888724            if (layer != this) {
    889                 layer.clearCurrentPhoto(false);
     725                layer.getImageData().clearSelectedImage();
    890726            }
    891727        }
    892728    }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    947783            public void mouseReleased(MouseEvent ev) {
    948784                if (ev.getButton() != MouseEvent.BUTTON1)
    949785                    return;
    950                 if (data == null || !isVisible() || !isMapModeOk())
     786                if (!isVisible() || !isMapModeOk())
    951787                    return;
    952788
    953789                Point mousePos = ev.getPoint();
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    956792                if (idx >= 0) {
    957793                    lastSelPos = mousePos;
    958794                    cycleModeArmed = false;
    959                     currentPhoto = idx;
    960                     showCurrentPhoto();
     795                    data.setSelectedImage(data.getImages().get(idx));
    961796                }
    962797            }
    963798        };
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    1012847        mapView.removeMouseMotionListener(mouseMotionAdapter);
    1013848        MapFrame.removeMapModeChangeListener(mapModeListener);
    1014849        MainApplication.getLayerManager().removeActiveLayerChangeListener(activeLayerChangeListener);
    1015         currentPhoto = -1;
    1016         if (data != null) {
    1017             data.clear();
    1018         }
    1019         data = null;
    1020850    }
    1021851
    1022852    @Override
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    1082912     * @return List of images in layer
    1083913     */
    1084914    public List<ImageEntry> getImages() {
    1085         return data == null ? Collections.<ImageEntry>emptyList() : new ArrayList<>(data);
     915        return new ArrayList<>(this.data.getImages());
     916    }
     917
     918    /**
     919     * Returns the image data store being used by this layer
     920     * @return imageData
     921     * @since xxx
     922     */
     923    public ImageData getImageData() {
     924        return data;
    1086925    }
    1087926
    1088927    /**
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    1127966        }
    1128967        invalidate();
    1129968    }
     969
     970    @Override
     971    public void selectedImageChanged(ImageData data) {
     972        this.showCurrentPhoto();
     973    }
    1130974}
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java
    index 53cf7ee8a..39d8da647 100644
    a b import java.awt.event.KeyEvent; 
    1313import java.awt.event.WindowEvent;
    1414import java.text.DateFormat;
    1515import java.text.SimpleDateFormat;
     16import java.util.Objects;
    1617
    1718import javax.swing.Box;
    1819import javax.swing.JButton;
     20import javax.swing.JLabel;
     21import javax.swing.JOptionPane;
    1922import javax.swing.JPanel;
    2023import javax.swing.JToggleButton;
     24import javax.swing.SwingConstants;
    2125
    2226import org.openstreetmap.josm.actions.JosmAction;
     27import org.openstreetmap.josm.data.ImageData;
     28import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
     29import org.openstreetmap.josm.gui.ExtendedDialog;
    2330import org.openstreetmap.josm.gui.MainApplication;
     31import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    2432import org.openstreetmap.josm.gui.dialogs.DialogsPanel.Action;
    2533import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
    2634import org.openstreetmap.josm.gui.layer.Layer;
    import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 
    3139import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
    3240import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
    3341import org.openstreetmap.josm.tools.ImageProvider;
     42import org.openstreetmap.josm.tools.Logging;
    3443import org.openstreetmap.josm.tools.Shortcut;
     44import org.openstreetmap.josm.tools.Utils;
    3545import org.openstreetmap.josm.tools.date.DateUtils;
    3646
    3747/**
    3848 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
    3949 */
    40 public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
     50public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener, ImageDataUpdateListener {
    4151
    4252    private final ImageZoomAction imageZoomAction = new ImageZoomAction();
    4353    private final ImageCenterViewAction imageCenterViewAction = new ImageCenterViewAction();
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    7989    private JButton btnPrevious;
    8090    private JButton btnFirst;
    8191    private JButton btnCollapse;
     92    private JButton btnDelete;
     93    private JButton btnCopyPath;
     94    private JButton btnDeleteFromDisk;
    8295    private JToggleButton tbCentre;
    8396
    8497    private ImageViewerDialog() {
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    87100        build();
    88101        MainApplication.getLayerManager().addActiveLayerChangeListener(this);
    89102        MainApplication.getLayerManager().addLayerChangeListener(this);
     103        for (Layer l: MainApplication.getLayerManager().getLayers()) {
     104            this.registerOnLayer(l);
     105        }
    90106    }
    91107
    92108    private static JButton createNavigationButton(JosmAction action, Dimension buttonDim) {
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    106122        btnFirst = createNavigationButton(imageFirstAction, buttonDim);
    107123        btnPrevious = createNavigationButton(imagePreviousAction, buttonDim);
    108124
    109         JButton btnDelete = new JButton(imageRemoveAction);
     125        btnDelete = new JButton(imageRemoveAction);
    110126        btnDelete.setPreferredSize(buttonDim);
    111127
    112         JButton btnDeleteFromDisk = new JButton(imageRemoveFromDiskAction);
     128        btnDeleteFromDisk = new JButton(imageRemoveFromDiskAction);
    113129        btnDeleteFromDisk.setPreferredSize(buttonDim);
    114130
    115         JButton btnCopyPath = new JButton(imageCopyPathAction);
     131        btnCopyPath = new JButton(imageCopyPathAction);
    116132        btnCopyPath.setPreferredSize(buttonDim);
    117133
    118134        btnNext = createNavigationButton(imageNextAction, buttonDim);
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    189205
    190206        @Override
    191207        public void actionPerformed(ActionEvent e) {
    192             if (currentLayer != null) {
    193                 currentLayer.showNextPhoto();
     208            if (currentData != null) {
     209                currentData.selectNextImage();
    194210            }
    195211        }
    196212    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    204220
    205221        @Override
    206222        public void actionPerformed(ActionEvent e) {
    207             if (currentLayer != null) {
    208                 currentLayer.showPreviousPhoto();
     223            if (currentData != null) {
     224                currentData.selectPreviousImage();
    209225            }
    210226        }
    211227    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    219235
    220236        @Override
    221237        public void actionPerformed(ActionEvent e) {
    222             if (currentLayer != null) {
    223                 currentLayer.showFirstPhoto();
     238            if (currentData != null) {
     239                currentData.selectFirstImage();
    224240            }
    225241        }
    226242    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    234250
    235251        @Override
    236252        public void actionPerformed(ActionEvent e) {
    237             if (currentLayer != null) {
    238                 currentLayer.showLastPhoto();
     253            if (currentData != null) {
     254                currentData.selectLastImage();
    239255            }
    240256        }
    241257    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    277293
    278294        @Override
    279295        public void actionPerformed(ActionEvent e) {
    280             if (currentLayer != null) {
    281                 currentLayer.removeCurrentPhoto();
     296            if (currentData != null) {
     297                currentData.removeSelectedImage();
    282298            }
    283299        }
    284300    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    293309
    294310        @Override
    295311        public void actionPerformed(ActionEvent e) {
    296             if (currentLayer != null) {
    297                 currentLayer.removeCurrentPhotoFromDisk();
     312            if (currentData != null && currentData.getSelectedImage() != null) {
     313                ImageEntry toDelete = currentData.getSelectedImage();
     314
     315                int result = new ExtendedDialog(
     316                        MainApplication.getMainFrame(),
     317                        tr("Delete image file from disk"),
     318                        tr("Cancel"), tr("Delete"))
     319                        .setButtonIcons("cancel", "dialogs/delete")
     320                        .setContent(new JLabel(tr("<html><h3>Delete the file {0} from disk?<p>The image file will be permanently lost!</h3></html>",
     321                                toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"), SwingConstants.LEFT))
     322                        .toggleEnable("geoimage.deleteimagefromdisk")
     323                        .setCancelButton(1)
     324                        .setDefaultButton(2)
     325                        .showDialog()
     326                        .getValue();
     327
     328                if (result == 2) {
     329                    currentData.removeSelectedImage();
     330
     331                    if (Utils.deleteFile(toDelete.getFile())) {
     332                        Logging.info("File "+toDelete.getFile()+" deleted. ");
     333                    } else {
     334                        JOptionPane.showMessageDialog(
     335                                MainApplication.getMainFrame(),
     336                                tr("Image file could not be deleted."),
     337                                tr("Error"),
     338                                JOptionPane.ERROR_MESSAGE
     339                                );
     340                    }
     341                }
    298342            }
    299343        }
    300344    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    308352
    309353        @Override
    310354        public void actionPerformed(ActionEvent e) {
    311             if (currentLayer != null) {
    312                 currentLayer.copyCurrentPhotoPath();
     355            if (currentData != null) {
     356                ClipboardUtils.copyString(currentData.getSelectedImage().getFile().toString());
    313357            }
    314358        }
    315359    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    329373
    330374    /**
    331375     * Displays image for the given layer.
    332      * @param layer geo image layer
     376     * @param data geo image layer
    333377     * @param entry image entry
    334378     */
    335     public static void showImage(GeoImageLayer layer, ImageEntry entry) {
    336         getInstance().displayImage(layer, entry);
    337         if (layer != null) {
    338             layer.checkPreviousNextButtons();
    339         } else {
    340             setPreviousEnabled(false);
    341             setNextEnabled(false);
    342         }
     379    public static void showImage(ImageData data, ImageEntry entry) {
     380        getInstance().displayImage(data, entry);
    343381    }
    344382
    345383    /**
    346384     * Enables (or disables) the "Previous" button.
    347385     * @param value {@code true} to enable the button, {@code false} otherwise
    348386     */
    349     public static void setPreviousEnabled(boolean value) {
    350         getInstance().btnFirst.setEnabled(value);
    351         getInstance().btnPrevious.setEnabled(value);
     387    public void setPreviousEnabled(boolean value) {
     388        this.btnFirst.setEnabled(value);
     389        this.btnPrevious.setEnabled(value);
    352390    }
    353391
    354392    /**
    355393     * Enables (or disables) the "Next" button.
    356394     * @param value {@code true} to enable the button, {@code false} otherwise
    357395     */
    358     public static void setNextEnabled(boolean value) {
    359         getInstance().btnNext.setEnabled(value);
    360         getInstance().btnLast.setEnabled(value);
     396    public void setNextEnabled(boolean value) {
     397        this.btnNext.setEnabled(value);
     398        this.btnLast.setEnabled(value);
    361399    }
    362400
    363401    /**
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    373411        return wasEnabled;
    374412    }
    375413
    376     private transient GeoImageLayer currentLayer;
     414    private transient ImageData currentData;
    377415    private transient ImageEntry currentEntry;
    378416
    379417    /**
    380418     * Displays image for the given layer.
    381      * @param layer geo image layer
     419     * @param data the image data
    382420     * @param entry image entry
    383421     */
    384     public void displayImage(GeoImageLayer layer, ImageEntry entry) {
     422    public void displayImage(ImageData data, ImageEntry entry) {
    385423        boolean imageChanged;
    386424
    387425        synchronized (this) {
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    393431                MainApplication.getMap().mapView.zoomTo(entry.getPos());
    394432            }
    395433
    396             currentLayer = layer;
     434            currentData = data;
    397435            currentEntry = entry;
    398436        }
    399437
     438
    400439        if (entry != null) {
     440            Objects.requireNonNull(data, "data cannot be null!");
     441            this.setNextEnabled(data.hasNextImage());
     442            this.setPreviousEnabled(data.hasPreviousImage());
     443            btnDelete.setEnabled(true);
     444            btnDeleteFromDisk.setEnabled(true);
     445            btnCopyPath.setEnabled(true);
     446
    401447            if (imageChanged) {
    402448                // Set only if the image is new to preserve zoom and position if the same image is redisplayed
    403449                // (e.g. to update the OSD).
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    436482            setTitle(tr("Geotagged Images"));
    437483            imgDisplay.setImage(null);
    438484            imgDisplay.setOsdText("");
     485            this.setNextEnabled(false);
     486            this.setPreviousEnabled(false);
     487            btnDelete.setEnabled(false);
     488            btnDeleteFromDisk.setEnabled(false);
     489            btnCopyPath.setEnabled(false);
    439490            return;
    440491        }
    441492        if (!isDialogShowing()) {
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    493544     * @since 6392
    494545     */
    495546    public static GeoImageLayer getCurrentLayer() {
    496         return getInstance().currentLayer;
     547        return null;
    497548    }
    498549
    499550    /**
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    507558
    508559    @Override
    509560    public void layerAdded(LayerAddEvent e) {
     561        this.registerOnLayer(e.getAddedLayer());
    510562        showLayer(e.getAddedLayer());
    511563    }
    512564
    513565    @Override
    514566    public void layerRemoving(LayerRemoveEvent e) {
    515         // Clear current image and layer if current layer is deleted
    516         if (currentLayer != null && currentLayer.equals(e.getRemovedLayer())) {
    517             showImage(null, null);
    518         }
    519         // Check buttons state in case of layer merging
    520         if (currentLayer != null && e.getRemovedLayer() instanceof GeoImageLayer) {
    521             currentLayer.checkPreviousNextButtons();
     567        if (e.getRemovedLayer() instanceof GeoImageLayer) {
     568            if (((GeoImageLayer) e.getRemovedLayer()).getImageData() == currentData) {
     569                displayImage(null, null);
     570            }
     571            ((GeoImageLayer) e.getRemovedLayer()).getImageData().removeImageDataUpdateListener(this);
    522572        }
    523573    }
    524574
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    532582        showLayer(e.getSource().getActiveLayer());
    533583    }
    534584
     585    private void registerOnLayer(Layer layer) {
     586        if (layer instanceof GeoImageLayer) {
     587            ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
     588        }
     589    }
     590
    535591    private void showLayer(Layer newLayer) {
    536         if (currentLayer == null && newLayer instanceof GeoImageLayer) {
    537             ((GeoImageLayer) newLayer).showFirstPhoto();
     592        if (currentData == null && newLayer instanceof GeoImageLayer) {
     593            ((GeoImageLayer) newLayer).getImageData().selectFirstImage();
    538594        }
    539595    }
     596
     597    @Override
     598    public void selectedImageChanged(ImageData data) {
     599        showImage(data, data.getSelectedImage());
     600    }
    540601}
  • src/org/openstreetmap/josm/gui/layer/geoimage/ShowThumbnailAction.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ShowThumbnailAction.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ShowThumbnailAction.java
    index 7a7b62791..5f75cad5f 100644
    a b public class ShowThumbnailAction extends AbstractAction implements LayerAction { 
    4949     *         {@code false} otherwise
    5050     */
    5151    private static boolean enabled(GeoImageLayer layer) {
    52         return layer.data != null && !layer.data.isEmpty();
     52        return !layer.getImageData().getImages().isEmpty();
    5353    }
    5454
    5555    /** Create actual menu entry and define if it is enabled or not. */
  • src/org/openstreetmap/josm/gui/layer/geoimage/ThumbsLoader.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ThumbsLoader.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ThumbsLoader.java
    index a49bbc8a3..0ead613c5 100644
    a b public class ThumbsLoader implements Runnable { 
    5151     * @param layer geoimage layer
    5252     */
    5353    public ThumbsLoader(GeoImageLayer layer) {
    54         this(new ArrayList<>(layer.data), layer);
     54        this(new ArrayList<>(layer.getImageData().getImages()), layer);
    5555    }
    5656
    5757    /**
  • new file test/unit/org/openstreetmap/josm/data/ImageDataTest.java

    diff --git a/test/unit/org/openstreetmap/josm/data/ImageDataTest.java b/test/unit/org/openstreetmap/josm/data/ImageDataTest.java
    new file mode 100644
    index 000000000..8ce314ff0
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data;
     3
     4import static org.junit.Assert.assertEquals;
     5import static org.junit.Assert.assertFalse;
     6import static org.junit.Assert.assertNull;
     7import static org.junit.Assert.assertTrue;
     8
     9import java.io.File;
     10import java.util.ArrayList;
     11import java.util.Collections;
     12import java.util.List;
     13
     14import org.junit.Test;
     15import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
     16import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
     17
     18import mockit.Expectations;
     19import mockit.Mock;
     20import mockit.MockUp;
     21
     22/**
     23 * Unit tests for class {@link ImageData}.
     24 */
     25public class ImageDataTest {
     26
     27    private List<ImageEntry> getOneImage() {
     28        ArrayList<ImageEntry> list = new ArrayList<>();
     29        list.add(new ImageEntry(new File("test")));
     30        return list;
     31    }
     32
     33    @Test
     34    public void testWithullData() {
     35        ImageData data = new ImageData();
     36        assertEquals(0, data.getImages().size());
     37        assertNull(data.getSelectedImage());
     38        data.selectFirstImage();
     39        assertNull(data.getSelectedImage());
     40        data.selectLastImage();
     41        assertNull(data.getSelectedImage());
     42        data.selectFirstImage();
     43        assertNull(data.getSelectedImage());
     44        data.selectPreviousImage();
     45        assertNull(data.getSelectedImage());
     46        assertFalse(data.hasNextImage());
     47        assertFalse(data.hasPreviousImage());
     48        data.removeSelectedImage();
     49    }
     50
     51    @Test
     52    public void testmageEntryWithImages() {
     53        assertEquals(1, new ImageData(this.getOneImage()).getImages().size());
     54    }
     55
     56    @Test
     57    public void testSortData() {
     58        List<ImageEntry> list = this.getOneImage();
     59
     60        new Expectations(Collections.class) {{
     61            Collections.sort(list);
     62        }};
     63
     64        new ImageData(list);
     65    }
     66
     67    @Test
     68    public void testIsModifiedFalse() {
     69        assertFalse(new ImageData(this.getOneImage()).isModified());
     70    }
     71
     72    @Test
     73    public void testIsModifiedTrue() {
     74        List<ImageEntry> list = this.getOneImage();
     75
     76        new Expectations(list.get(0)) {{
     77            list.get(0).hasNewGpsData(); result = true;
     78        }};
     79
     80        assertTrue(new ImageData(list).isModified());
     81    }
     82
     83    @Test
     84    public void testSelectFirstImage() {
     85        List<ImageEntry> list = this.getOneImage();
     86
     87        ImageData data = new ImageData(list);
     88        data.selectFirstImage();
     89        assertEquals(list.get(0), data.getSelectedImage());
     90    }
     91
     92    @Test
     93    public void testSelectLastImage() {
     94        List<ImageEntry> list = this.getOneImage();
     95        list.add(new ImageEntry());
     96
     97        ImageData data = new ImageData(list);
     98        data.selectLastImage();
     99        assertEquals(list.get(1), data.getSelectedImage());
     100    }
     101
     102    @Test
     103    public void testSelectNextImage() {
     104        List<ImageEntry> list = this.getOneImage();
     105
     106        ImageData data = new ImageData(list);
     107        assertTrue(data.hasNextImage());
     108        data.selectNextImage();
     109        assertEquals(list.get(0), data.getSelectedImage());
     110        assertFalse(data.hasNextImage());
     111        data.selectNextImage();
     112        assertEquals(list.get(0), data.getSelectedImage());
     113    }
     114
     115    @Test
     116    public void testSelectPreviousImage() {
     117        List<ImageEntry> list = this.getOneImage();
     118        list.add(new ImageEntry());
     119
     120        ImageData data = new ImageData(list);
     121        assertFalse(data.hasPreviousImage());
     122        data.selectLastImage();
     123        assertTrue(data.hasPreviousImage());
     124        data.selectPreviousImage();
     125        assertEquals(list.get(0), data.getSelectedImage());
     126        data.selectPreviousImage();
     127        assertEquals(list.get(0), data.getSelectedImage());
     128    }
     129
     130    @Test
     131    public void testSetSelectedImage() {
     132        List<ImageEntry> list = this.getOneImage();
     133
     134        ImageData data = new ImageData(list);
     135        data.setSelectedImage(list.get(0));
     136        assertEquals(list.get(0), data.getSelectedImage());
     137    }
     138
     139    @Test
     140    public void testClearSelectedImage() {
     141        List<ImageEntry> list = this.getOneImage();
     142
     143        ImageData data = new ImageData(list);
     144        data.setSelectedImage(list.get(0));
     145        data.clearSelectedImage();
     146        assertNull(data.getSelectedImage());
     147    }
     148
     149    @Test
     150    public void testSelectionListener() {
     151        List<ImageEntry> list = this.getOneImage();
     152        ImageData data = new ImageData(list);
     153        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     154            @Override
     155            public void selectedImageChanged(ImageData data) {}
     156        };
     157        new Expectations(listener) {{
     158            listener.selectedImageChanged(data); times = 1;
     159        }};
     160        data.addImageDataUpdateListener(listener);
     161        data.selectFirstImage();
     162        data.selectFirstImage();
     163    }
     164
     165    @Test
     166    public void testRemoveSelectedImage() {
     167        List<ImageEntry> list = this.getOneImage();
     168        ImageData data = new ImageData(list);
     169        data.selectFirstImage();
     170        data.removeSelectedImage();
     171        assertEquals(0, data.getImages().size());
     172        assertNull(data.getSelectedImage());
     173    }
     174
     175    @Test
     176    public void testRemoveSelectedWithImageTriggerListener() {
     177        List<ImageEntry> list = this.getOneImage();
     178        list.add(new ImageEntry());
     179        ImageData data = new ImageData(list);
     180        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     181            @Override
     182            public void selectedImageChanged(ImageData data) {}
     183        };
     184        new Expectations(listener) {{
     185            listener.selectedImageChanged(data); times = 2;
     186        }};
     187        data.addImageDataUpdateListener(listener);
     188        data.selectFirstImage();
     189        data.removeSelectedImage();
     190    }
     191
     192    @Test
     193    public void testMergeFrom() {
     194        ImageEntry image = new ImageEntry(new File("test2"));
     195        List<ImageEntry> list1 = this.getOneImage();
     196        list1.add(image);
     197        List<ImageEntry> list2 = this.getOneImage();
     198        list2.add(new ImageEntry(new File("test3")));
     199
     200        ImageData data = new ImageData(list1);
     201        data.setSelectedImage(list1.get(0));
     202        ImageData data2 = new ImageData(list2);
     203
     204        new MockUp<Collections>() {
     205            @Mock
     206            public void sort(List<ImageEntry> o) {
     207                list1.remove(image);
     208                list1.add(image);
     209            }
     210        };
     211
     212        data.mergeFrom(data2);
     213        assertEquals(3, data.getImages().size());
     214        assertEquals(list1.get(0), data.getSelectedImage());
     215    }
     216
     217    @Test
     218    public void testMergeFromSelectedImage() {
     219        ImageEntry image = new ImageEntry(new File("test2"));
     220        List<ImageEntry> list1 = this.getOneImage();
     221        list1.add(image);
     222        List<ImageEntry> list2 = this.getOneImage();
     223
     224        ImageData data = new ImageData(list1);
     225        ImageData data2 = new ImageData(list2);
     226        data2.setSelectedImage(list2.get(0));
     227
     228        data.mergeFrom(data2);
     229        assertEquals(3, data.getImages().size());
     230        assertEquals(list2.get(0), data.getSelectedImage());
     231    }
     232}