Ticket #17050: refactor_geoimagelayer.4.patch

File refactor_geoimagelayer.4.patch, 61.9 KB (added by francois2, 2 months ago)

Patch v3.1

  • 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..6dab9ae5f
    - +  
     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.data.coor.LatLon;
     9import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
     10import org.openstreetmap.josm.tools.ListenerList;
     11
     12/**
     13 * Class to hold {@link ImageEntry} and the current selection
     14 * @since xxx
     15 */
     16public class ImageData {
     17    /**
     18     * A listener that is informed when the current selection change
     19     */
     20    public interface ImageDataUpdateListener {
     21        /**
     22         * Called when the data change
     23         * @param data the image data
     24         */
     25        void imageDataUpdated(ImageData data);
     26
     27        /**
     28         * Called when the selection change
     29         * @param data the image data
     30         */
     31        void selectedImageChanged(ImageData data);
     32    }
     33
     34    private final List<ImageEntry> data;
     35
     36    private int selectedImageIndex = -1;
     37
     38    private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create();
     39
     40    /**
     41     * Construct a new image container without images
     42     */
     43    public ImageData() {
     44        this(null);
     45    }
     46
     47    /**
     48     * Construct a new image container with a list of images
     49     * @param data the list of {@link ImageEntry}
     50     */
     51    public ImageData(List<ImageEntry> data) {
     52        if (data != null) {
     53            Collections.sort(data);
     54            this.data = data;
     55        } else {
     56            this.data = new ArrayList<>();
     57        }
     58    }
     59
     60    /**
     61     * Returns the images
     62     * @return the images
     63     */
     64    public List<ImageEntry> getImages() {
     65        return this.data;
     66    }
     67
     68    /**
     69     * Determines if one image has modified GPS data.
     70     * @return {@code true} if data has been modified; {@code false}, otherwise
     71     */
     72    public boolean isModified() {
     73        for (ImageEntry e : data) {
     74            if (e.hasNewGpsData()) {
     75                return true;
     76            }
     77        }
     78        return false;
     79    }
     80
     81    /**
     82     * Merge 2 ImageData
     83     * @param data {@link ImageData}
     84     */
     85    public void mergeFrom(ImageData data) {
     86        this.data.addAll(data.getImages());
     87        Collections.sort(this.data);
     88
     89        final ImageEntry selected = data.getSelectedImage();
     90
     91        // Suppress the double photos.
     92        if (this.data.size() > 1) {
     93            ImageEntry cur;
     94            ImageEntry prev = this.data.get(this.data.size() - 1);
     95            for (int i = this.data.size() - 2; i >= 0; i--) {
     96                cur = this.data.get(i);
     97                if (cur.getFile().equals(prev.getFile())) {
     98                    this.data.remove(i);
     99                } else {
     100                    prev = cur;
     101                }
     102            }
     103        }
     104        if (selected != null) {
     105            this.setSelectedImageIndex(this.data.indexOf(selected));
     106        }
     107    }
     108
     109    /**
     110     * Return the current selected image
     111     * @return the selected image as {@link ImageEntry} or null
     112     */
     113    public ImageEntry getSelectedImage() {
     114        if (this.selectedImageIndex > -1) {
     115            return data.get(this.selectedImageIndex);
     116        }
     117        return null;
     118    }
     119
     120    /**
     121     * Select the first image of the sequence
     122     */
     123    public void selectFirstImage() {
     124        if (!data.isEmpty()) {
     125            this.setSelectedImageIndex(0);
     126        }
     127    }
     128
     129    /**
     130     * Select the last image of the sequence
     131     */
     132    public void selectLastImage() {
     133        this.setSelectedImageIndex(data.size() - 1);
     134    }
     135
     136    /**
     137     * Check if there is a next image in the sequence
     138     * @return {@code true} is there is a next image, {@code false} otherwise
     139     */
     140    public boolean hasNextImage() {
     141        return (this.selectedImageIndex != data.size() - 1);
     142    }
     143
     144    /**
     145     * Select the next image of the sequence
     146     */
     147    public void selectNextImage() {
     148        if (this.hasNextImage()) {
     149            this.setSelectedImageIndex(this.selectedImageIndex + 1);
     150        }
     151    }
     152
     153    /**
     154     *  Check if there is a previous image in the sequence
     155     * @return {@code true} is there is a previous image, {@code false} otherwise
     156     */
     157    public boolean hasPreviousImage() {
     158        return this.selectedImageIndex - 1 > -1;
     159    }
     160
     161    /**
     162     * Select the previous image of the sequence
     163     */
     164    public void selectPreviousImage() {
     165        if (data.isEmpty()) {
     166            return;
     167        }
     168        this.setSelectedImageIndex(Integer.max(0, this.selectedImageIndex - 1));
     169    }
     170
     171    /**
     172     * Select as the selected the given image
     173     * @param image the selected image
     174     */
     175    public void setSelectedImage(ImageEntry image) {
     176        this.setSelectedImageIndex(this.data.indexOf(image));
     177    }
     178
     179    /**
     180     * Clear the selected image
     181     */
     182    public void clearSelectedImage() {
     183        this.setSelectedImageIndex(-1);
     184    }
     185
     186    private void setSelectedImageIndex(int index) {
     187        this.setSelectedImageIndex(index, false);
     188    }
     189
     190    private void setSelectedImageIndex(int index, boolean forceTrigger) {
     191        if (index == this.selectedImageIndex && !forceTrigger) {
     192            return;
     193        }
     194        this.selectedImageIndex = index;
     195        listeners.fireEvent(l -> l.selectedImageChanged(this));
     196    }
     197
     198    /**
     199     * Remove the current selected image from the list
     200     */
     201    public void removeSelectedImage() {
     202        data.remove(this.getSelectedImage());
     203        if (this.selectedImageIndex == data.size()) {
     204            this.setSelectedImageIndex(data.size() - 1);
     205        } else {
     206            this.setSelectedImageIndex(this.selectedImageIndex, true);
     207        }
     208    }
     209
     210    /**
     211     * Remove the image from the list and trigger update listener
     212     * @param img the {@link ImageEntry} to remove
     213     */
     214    public void removeImage(ImageEntry img) {
     215        data.remove(img);
     216        this.notifyImageUpdate();
     217    }
     218
     219    /**
     220     * Update the position of the image and trigger update
     221     * @param img the image to update
     222     * @param newPos the new position
     223     */
     224    public void updateImagePosition(ImageEntry img, LatLon newPos) {
     225        img.setPos(newPos);
     226        this.afterImageUpdated(img);
     227    }
     228
     229    /**
     230     * Update the image direction of the image and trigger update
     231     * @param img the image to update
     232     * @param direction the new direction
     233     */
     234    public void updateImageDirection(ImageEntry img, double direction) {
     235        img.setExifImgDir(direction);
     236        this.afterImageUpdated(img);
     237    }
     238
     239    /**
     240     * Manually trigger the {@link ImageDataUpdateListener#imageDataUpdated(ImageData)}
     241     */
     242    public void notifyImageUpdate() {
     243        listeners.fireEvent(l -> l.imageDataUpdated(this));
     244    }
     245
     246    private void afterImageUpdated(ImageEntry img) {
     247        img.flagNewGpsData();
     248        this.notifyImageUpdate();
     249    }
     250
     251    /**
     252     * Add a listener that listens to image data changes
     253     * @param listener the {@link ImageDataUpdateListener}
     254     */
     255    public void addImageDataUpdateListener(ImageDataUpdateListener listener) {
     256        listeners.addListener(listener);
     257    }
     258
     259    /**
     260     * Removes a listener that listens to image data changes
     261     * @param listener The listener
     262     */
     263    public void removeImageDataUpdateListener(ImageDataUpdateListener listener) {
     264        listeners.removeListener(listener);
     265    }
     266}
  • 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 a9bb91f33..a4c032612 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 
    311304        }
    312305    }
    313306
     307    /**
     308     * Create a GeoImageLayer asynchronously
     309     * @param files the list of image files to display
     310     * @param gpxLayer the gpx layer
     311     */
    314312    public static void create(Collection<File> files, GpxLayer gpxLayer) {
    315313        MainApplication.worker.execute(new Loader(files, gpxLayer));
    316314    }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    320318        return ImageProvider.get("dialogs/geoimage", ImageProvider.ImageSizes.LAYER);
    321319    }
    322320
     321    /**
     322     * Register actions on the layer
     323     * @param addition the action to be added
     324     */
    323325    public static void registerMenuAddition(Action addition) {
    324326        menuAdditions.add(addition);
    325327    }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    356358    private String infoText() {
    357359        int tagged = 0;
    358360        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                 }
     361        int n = data.getImages().size();
     362        for (ImageEntry e : data.getImages()) {
     363            if (e.getPos() != null) {
     364                tagged++;
     365            }
     366            if (e.hasNewGpsData()) {
     367                newdata++;
    369368            }
    370369        }
    371370        return "<html>"
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    391390     */
    392391    @Override
    393392    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;
     393        return this.data.isModified();
    402394    }
    403395
    404396    @Override
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    417409        stopLoadThumbs();
    418410        l.stopLoadThumbs();
    419411
    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         }
     412        this.data.mergeFrom(l.getImageData());
    452413
    453414        setName(l.getName());
    454415        thumbsLoaded &= l.thumbsLoaded;
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    530491                tempG.fillRect(0, 0, width, height);
    531492                tempG.setComposite(saveComp);
    532493
    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                     }
     494                for (ImageEntry e : this.data.getImages()) {
     495                    paintImage(e, mv, clip, tempG);
     496                }
     497                if (this.data.getSelectedImage() != null) {
     498                    // Make sure the selected image is on top in case multiple images overlap.
     499                    paintImage(this.data.getSelectedImage(), mv, clip, tempG);
    541500                }
    542501                updateOffscreenBuffer = false;
    543502            }
    544503            g.drawImage(offscreenBuffer, 0, 0, null);
    545         } else if (data != null) {
    546             for (ImageEntry e : data) {
     504        } else {
     505            for (ImageEntry e : data.getImages()) {
    547506                if (e.getPos() == null) {
    548507                    continue;
    549508                }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    554513            }
    555514        }
    556515
    557         if (currentPhoto >= 0 && currentPhoto < data.size()) {
    558             ImageEntry e = data.get(currentPhoto);
    559 
     516        ImageEntry e = data.getSelectedImage();
     517        if (e != null) {
    560518            if (e.getPos() != null) {
    561519                Point p = mv.getPoint(e.getPos());
    562520
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    621579
    622580    @Override
    623581    public void visitBoundingBox(BoundingXYVisitor v) {
    624         for (ImageEntry e : data) {
     582        for (ImageEntry e : data.getImages()) {
    625583            v.visit(e.getPos());
    626584        }
    627585    }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    630588     * Show current photo on map and in image viewer.
    631589     */
    632590    public void showCurrentPhoto() {
    633         clearOtherCurrentPhotos();
    634         if (currentPhoto >= 0) {
    635             ImageViewerDialog.showImage(this, data.get(currentPhoto));
    636         } else {
    637             ImageViewerDialog.showImage(this, null);
     591        if (data.getSelectedImage() != null) {
     592            clearOtherCurrentPhotos();
    638593        }
    639594        updateBufferAndRepaint();
    640595    }
    641596
    642     /**
    643      * Shows next photo.
    644      */
    645     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();
    655     }
    656 
    657     /**
    658      * Shows previous photo.
    659      */
    660     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();
    670     }
    671 
    672     /**
    673      * Shows first photo.
    674      */
    675     public void showFirstPhoto() {
    676         if (data != null && !data.isEmpty()) {
    677             currentPhoto = 0;
    678         } else {
    679             currentPhoto = -1;
    680         }
    681         showCurrentPhoto();
    682     }
    683 
    684     /**
    685      * Shows last photo.
    686      */
    687     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);
    699     }
    700 
    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     }
    767 
    768597    /**
    769598     * 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
     599     * @param idx the image index
    771600     * @param evt Mouse event
    772601     * @return {@code true} if the photo matches the mouse position, {@code false} otherwise
    773602     */
    774603    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                     }
     604        ImageEntry img = this.data.getImages().get(idx);
     605        if (img.getPos() != null) {
     606            Point imgCenter = MainApplication.getMap().mapView.getPoint(img.getPos());
     607            Rectangle imgRect;
     608            if (useThumbs && img.hasThumbnail()) {
     609                Dimension imgDim = scaledDimension(img.getThumbnail());
     610                if (imgDim != null) {
     611                    imgRect = new Rectangle(imgCenter.x - imgDim.width / 2,
     612                                            imgCenter.y - imgDim.height / 2,
     613                                            imgDim.width, imgDim.height);
    789614                } 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;
     615                    imgRect = null;
    796616                }
     617            } else {
     618                imgRect = new Rectangle(imgCenter.x - icon.getIconWidth() / 2,
     619                                        imgCenter.y - icon.getIconHeight() / 2,
     620                                        icon.getIconWidth(), icon.getIconHeight());
     621            }
     622            if (imgRect != null && imgRect.contains(evt.getPoint())) {
     623                return true;
    797624            }
    798625        }
    799626        return false;
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    809636     *               or {@code -1} if there is no image at the mouse position
    810637     */
    811638    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                     }
     639        ImageEntry selectedImage = this.data.getSelectedImage();
     640        int selectedIndex = this.data.getImages().indexOf(selectedImage);
     641
     642        if (cycle && selectedImage != null) {
     643            // Cycle loop is forward as that is the natural order.
     644            // Loop 1: One after current photo up to last one.
     645            for (int idx = selectedIndex + 1; idx < this.data.getImages().size(); ++idx) {
     646                if (isPhotoIdxUnderMouse(idx, evt)) {
     647                    return idx;
    820648                }
    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                     }
    826                 }
    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;
     649            }
     650            // Loop 2: First photo up to current one.
     651            for (int idx = 0; idx <= selectedIndex; ++idx) {
     652                if (isPhotoIdxUnderMouse(idx, evt)) {
     653                    return idx;
    831654                }
    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                     }
     655            }
     656        } else {
     657            // Check for current photo first, i.e. keep it selected if it is under the mouse.
     658            if (selectedImage != null && isPhotoIdxUnderMouse(selectedIndex, evt)) {
     659                return selectedIndex;
     660            }
     661            // Loop from last to first to prefer topmost image.
     662            for (int idx = this.data.getImages().size() - 1; idx >= 0; --idx) {
     663                if (isPhotoIdxUnderMouse(idx, evt)) {
     664                    return idx;
    837665                }
    838666            }
    839667        }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    861689    public ImageEntry getPhotoUnderMouse(MouseEvent evt) {
    862690        int idx = getPhotoIdxUnderMouse(evt);
    863691        if (idx >= 0) {
    864             return data.get(idx);
     692            return this.data.getImages().get(idx);
    865693        } else {
    866694            return null;
    867695        }
    868696    }
    869697
    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 
    882698    /**
    883699     * Clears the currentPhoto of the other GeoImageLayer's. Otherwise there could be multiple selected photos.
    884700     */
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    886702        for (GeoImageLayer layer:
    887703                 MainApplication.getLayerManager().getLayersOfType(GeoImageLayer.class)) {
    888704            if (layer != this) {
    889                 layer.clearCurrentPhoto(false);
     705                layer.getImageData().clearSelectedImage();
    890706            }
    891707        }
    892708    }
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    947763            public void mouseReleased(MouseEvent ev) {
    948764                if (ev.getButton() != MouseEvent.BUTTON1)
    949765                    return;
    950                 if (data == null || !isVisible() || !isMapModeOk())
     766                if (!isVisible() || !isMapModeOk())
    951767                    return;
    952768
    953769                Point mousePos = ev.getPoint();
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    956772                if (idx >= 0) {
    957773                    lastSelPos = mousePos;
    958774                    cycleModeArmed = false;
    959                     currentPhoto = idx;
    960                     showCurrentPhoto();
     775                    data.setSelectedImage(data.getImages().get(idx));
    961776                }
    962777            }
    963778        };
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    1013828        MapView.removeZoomChangeListener(this);
    1014829        MapFrame.removeMapModeChangeListener(mapModeListener);
    1015830        MainApplication.getLayerManager().removeActiveLayerChangeListener(activeLayerChangeListener);
    1016         currentPhoto = -1;
    1017         if (data != null) {
    1018             data.clear();
    1019         }
    1020         data = null;
     831        this.data.removeImageDataUpdateListener(this);
    1021832    }
    1022833
    1023834    @Override
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    1083894     * @return List of images in layer
    1084895     */
    1085896    public List<ImageEntry> getImages() {
    1086         return data == null ? Collections.<ImageEntry>emptyList() : new ArrayList<>(data);
     897        return new ArrayList<>(this.data.getImages());
     898    }
     899
     900    /**
     901     * Returns the image data store being used by this layer
     902     * @return imageData
     903     * @since xxx
     904     */
     905    public ImageData getImageData() {
     906        return data;
    1087907    }
    1088908
    1089909    /**
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    1096916
    1097917    @Override
    1098918    public void jumpToNextMarker() {
    1099         showNextPhoto();
     919        this.data.selectNextImage();
    1100920    }
    1101921
    1102922    @Override
    1103923    public void jumpToPreviousMarker() {
    1104         showPreviousPhoto();
     924        this.data.selectPreviousImage();
    1105925    }
    1106926
    1107927    /**
    public class GeoImageLayer extends AbstractModifiableLayer implements 
    1128948        }
    1129949        invalidate();
    1130950    }
     951
     952    @Override
     953    public void selectedImageChanged(ImageData data) {
     954        this.showCurrentPhoto();
     955    }
     956
     957    @Override
     958    public void imageDataUpdated(ImageData data) {
     959        updateBufferAndRepaint();
     960    }
    1131961}
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java

    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java
    index eb367026f..cba56207b 100644
    a b public class ImageDisplay extends JComponent implements Destroyable, PreferenceC 
    702702     * @param text text to display on top of the image
    703703     */
    704704    public void setOsdText(String text) {
    705         this.osdText = text;
    706         repaint();
     705        if (!text.equals(this.osdText)) {
     706            this.osdText = text;
     707            repaint();
     708        }
    707709    }
    708710
    709711    @Override
  • 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..5fd27fa5a 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?"
     321                                + "<p>The image file will be permanently lost!</h3></html>",
     322                                toDelete.getFile().getName()), ImageProvider.get("dialogs/geoimage/deletefromdisk"), SwingConstants.LEFT))
     323                        .toggleEnable("geoimage.deleteimagefromdisk")
     324                        .setCancelButton(1)
     325                        .setDefaultButton(2)
     326                        .showDialog()
     327                        .getValue();
     328
     329                if (result == 2) {
     330                    currentData.removeSelectedImage();
     331
     332                    if (Utils.deleteFile(toDelete.getFile())) {
     333                        Logging.info("File "+toDelete.getFile()+" deleted. ");
     334                    } else {
     335                        JOptionPane.showMessageDialog(
     336                                MainApplication.getMainFrame(),
     337                                tr("Image file could not be deleted."),
     338                                tr("Error"),
     339                                JOptionPane.ERROR_MESSAGE
     340                                );
     341                    }
     342                }
    298343            }
    299344        }
    300345    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    308353
    309354        @Override
    310355        public void actionPerformed(ActionEvent e) {
    311             if (currentLayer != null) {
    312                 currentLayer.copyCurrentPhotoPath();
     356            if (currentData != null) {
     357                ClipboardUtils.copyString(currentData.getSelectedImage().getFile().toString());
    313358            }
    314359        }
    315360    }
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    329374
    330375    /**
    331376     * Displays image for the given layer.
    332      * @param layer geo image layer
     377     * @param data geo image layer
    333378     * @param entry image entry
    334379     */
    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         }
     380    public static void showImage(ImageData data, ImageEntry entry) {
     381        getInstance().displayImage(data, entry);
    343382    }
    344383
    345384    /**
    346385     * Enables (or disables) the "Previous" button.
    347386     * @param value {@code true} to enable the button, {@code false} otherwise
    348387     */
    349     public static void setPreviousEnabled(boolean value) {
    350         getInstance().btnFirst.setEnabled(value);
    351         getInstance().btnPrevious.setEnabled(value);
     388    public void setPreviousEnabled(boolean value) {
     389        this.btnFirst.setEnabled(value);
     390        this.btnPrevious.setEnabled(value);
    352391    }
    353392
    354393    /**
    355394     * Enables (or disables) the "Next" button.
    356395     * @param value {@code true} to enable the button, {@code false} otherwise
    357396     */
    358     public static void setNextEnabled(boolean value) {
    359         getInstance().btnNext.setEnabled(value);
    360         getInstance().btnLast.setEnabled(value);
     397    public void setNextEnabled(boolean value) {
     398        this.btnNext.setEnabled(value);
     399        this.btnLast.setEnabled(value);
    361400    }
    362401
    363402    /**
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    373412        return wasEnabled;
    374413    }
    375414
    376     private transient GeoImageLayer currentLayer;
     415    private transient ImageData currentData;
    377416    private transient ImageEntry currentEntry;
    378417
    379418    /**
    380419     * Displays image for the given layer.
    381      * @param layer geo image layer
     420     * @param data the image data
    382421     * @param entry image entry
    383422     */
    384     public void displayImage(GeoImageLayer layer, ImageEntry entry) {
     423    public void displayImage(ImageData data, ImageEntry entry) {
    385424        boolean imageChanged;
    386425
    387426        synchronized (this) {
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    393432                MainApplication.getMap().mapView.zoomTo(entry.getPos());
    394433            }
    395434
    396             currentLayer = layer;
     435            currentData = data;
    397436            currentEntry = entry;
    398437        }
    399438
     439
    400440        if (entry != null) {
     441            Objects.requireNonNull(data, "data cannot be null!");
     442            this.setNextEnabled(data.hasNextImage());
     443            this.setPreviousEnabled(data.hasPreviousImage());
     444            btnDelete.setEnabled(true);
     445            btnDeleteFromDisk.setEnabled(true);
     446            btnCopyPath.setEnabled(true);
     447
    401448            if (imageChanged) {
    402449                // Set only if the image is new to preserve zoom and position if the same image is redisplayed
    403450                // (e.g. to update the OSD).
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    436483            setTitle(tr("Geotagged Images"));
    437484            imgDisplay.setImage(null);
    438485            imgDisplay.setOsdText("");
     486            this.setNextEnabled(false);
     487            this.setPreviousEnabled(false);
     488            btnDelete.setEnabled(false);
     489            btnDeleteFromDisk.setEnabled(false);
     490            btnCopyPath.setEnabled(false);
    439491            return;
    440492        }
    441493        if (!isDialogShowing()) {
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    487539        return getInstance().currentEntry;
    488540    }
    489541
    490     /**
    491      * Returns the layer associated with the image.
    492      * @return Layer associated with the image
    493      * @since 6392
    494      */
    495     public static GeoImageLayer getCurrentLayer() {
    496         return getInstance().currentLayer;
    497     }
    498 
    499542    /**
    500543     * Returns whether the center view is currently active.
    501544     * @return {@code true} if the center view is active, {@code false} otherwise
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    507550
    508551    @Override
    509552    public void layerAdded(LayerAddEvent e) {
     553        this.registerOnLayer(e.getAddedLayer());
    510554        showLayer(e.getAddedLayer());
    511555    }
    512556
    513557    @Override
    514558    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();
     559        if (e.getRemovedLayer() instanceof GeoImageLayer) {
     560            if (((GeoImageLayer) e.getRemovedLayer()).getImageData() == currentData) {
     561                displayImage(null, null);
     562            }
     563            ((GeoImageLayer) e.getRemovedLayer()).getImageData().removeImageDataUpdateListener(this);
    522564        }
    523565    }
    524566
    public final class ImageViewerDialog extends ToggleDialog implements LayerChange 
    532574        showLayer(e.getSource().getActiveLayer());
    533575    }
    534576
     577    private void registerOnLayer(Layer layer) {
     578        if (layer instanceof GeoImageLayer) {
     579            ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
     580        }
     581    }
     582
    535583    private void showLayer(Layer newLayer) {
    536         if (currentLayer == null && newLayer instanceof GeoImageLayer) {
    537             ((GeoImageLayer) newLayer).showFirstPhoto();
     584        if (currentData == null && newLayer instanceof GeoImageLayer) {
     585            ((GeoImageLayer) newLayer).getImageData().selectFirstImage();
    538586        }
    539587    }
     588
     589    @Override
     590    public void selectedImageChanged(ImageData data) {
     591        showImage(data, data.getSelectedImage());
     592    }
     593
     594    @Override
     595    public void imageDataUpdated(ImageData data) {
     596        showImage(data, data.getSelectedImage());
     597    }
    540598}
  • 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..a79374ff2
    - +  
     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.data.coor.LatLon;
     17import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
     18
     19import mockit.Expectations;
     20import mockit.Mock;
     21import mockit.MockUp;
     22
     23/**
     24 * Unit tests for class {@link ImageData}.
     25 */
     26public class ImageDataTest {
     27
     28    private List<ImageEntry> getOneImage() {
     29        ArrayList<ImageEntry> list = new ArrayList<>();
     30        list.add(new ImageEntry(new File("test")));
     31        return list;
     32    }
     33
     34    @Test
     35    public void testWithullData() {
     36        ImageData data = new ImageData();
     37        assertEquals(0, data.getImages().size());
     38        assertNull(data.getSelectedImage());
     39        data.selectFirstImage();
     40        assertNull(data.getSelectedImage());
     41        data.selectLastImage();
     42        assertNull(data.getSelectedImage());
     43        data.selectFirstImage();
     44        assertNull(data.getSelectedImage());
     45        data.selectPreviousImage();
     46        assertNull(data.getSelectedImage());
     47        assertFalse(data.hasNextImage());
     48        assertFalse(data.hasPreviousImage());
     49        data.removeSelectedImage();
     50    }
     51
     52    @Test
     53    public void testmageEntryWithImages() {
     54        assertEquals(1, new ImageData(this.getOneImage()).getImages().size());
     55    }
     56
     57    @Test
     58    public void testSortData() {
     59        List<ImageEntry> list = this.getOneImage();
     60
     61        new Expectations(Collections.class) {{
     62            Collections.sort(list);
     63        }};
     64
     65        new ImageData(list);
     66    }
     67
     68    @Test
     69    public void testIsModifiedFalse() {
     70        assertFalse(new ImageData(this.getOneImage()).isModified());
     71    }
     72
     73    @Test
     74    public void testIsModifiedTrue() {
     75        List<ImageEntry> list = this.getOneImage();
     76
     77        new Expectations(list.get(0)) {{
     78            list.get(0).hasNewGpsData(); result = true;
     79        }};
     80
     81        assertTrue(new ImageData(list).isModified());
     82    }
     83
     84    @Test
     85    public void testSelectFirstImage() {
     86        List<ImageEntry> list = this.getOneImage();
     87
     88        ImageData data = new ImageData(list);
     89        data.selectFirstImage();
     90        assertEquals(list.get(0), data.getSelectedImage());
     91    }
     92
     93    @Test
     94    public void testSelectLastImage() {
     95        List<ImageEntry> list = this.getOneImage();
     96        list.add(new ImageEntry());
     97
     98        ImageData data = new ImageData(list);
     99        data.selectLastImage();
     100        assertEquals(list.get(1), data.getSelectedImage());
     101    }
     102
     103    @Test
     104    public void testSelectNextImage() {
     105        List<ImageEntry> list = this.getOneImage();
     106
     107        ImageData data = new ImageData(list);
     108        assertTrue(data.hasNextImage());
     109        data.selectNextImage();
     110        assertEquals(list.get(0), data.getSelectedImage());
     111        assertFalse(data.hasNextImage());
     112        data.selectNextImage();
     113        assertEquals(list.get(0), data.getSelectedImage());
     114    }
     115
     116    @Test
     117    public void testSelectPreviousImage() {
     118        List<ImageEntry> list = this.getOneImage();
     119        list.add(new ImageEntry());
     120
     121        ImageData data = new ImageData(list);
     122        assertFalse(data.hasPreviousImage());
     123        data.selectLastImage();
     124        assertTrue(data.hasPreviousImage());
     125        data.selectPreviousImage();
     126        assertEquals(list.get(0), data.getSelectedImage());
     127        data.selectPreviousImage();
     128        assertEquals(list.get(0), data.getSelectedImage());
     129    }
     130
     131    @Test
     132    public void testSetSelectedImage() {
     133        List<ImageEntry> list = this.getOneImage();
     134
     135        ImageData data = new ImageData(list);
     136        data.setSelectedImage(list.get(0));
     137        assertEquals(list.get(0), data.getSelectedImage());
     138    }
     139
     140    @Test
     141    public void testClearSelectedImage() {
     142        List<ImageEntry> list = this.getOneImage();
     143
     144        ImageData data = new ImageData(list);
     145        data.setSelectedImage(list.get(0));
     146        data.clearSelectedImage();
     147        assertNull(data.getSelectedImage());
     148    }
     149
     150    @Test
     151    public void testSelectionListener() {
     152        List<ImageEntry> list = this.getOneImage();
     153        ImageData data = new ImageData(list);
     154        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     155            @Override
     156            public void selectedImageChanged(ImageData data) {}
     157
     158            @Override
     159            public void imageDataUpdated(ImageData data) {}
     160        };
     161        new Expectations(listener) {{
     162            listener.selectedImageChanged(data); times = 1;
     163        }};
     164        data.addImageDataUpdateListener(listener);
     165        data.selectFirstImage();
     166        data.selectFirstImage();
     167    }
     168
     169    @Test
     170    public void testRemoveSelectedImage() {
     171        List<ImageEntry> list = this.getOneImage();
     172        ImageData data = new ImageData(list);
     173        data.selectFirstImage();
     174        data.removeSelectedImage();
     175        assertEquals(0, data.getImages().size());
     176        assertNull(data.getSelectedImage());
     177    }
     178
     179    @Test
     180    public void testRemoveSelectedWithImageTriggerListener() {
     181        List<ImageEntry> list = this.getOneImage();
     182        list.add(new ImageEntry());
     183        ImageData data = new ImageData(list);
     184        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     185            @Override
     186            public void selectedImageChanged(ImageData data) {}
     187
     188            @Override
     189            public void imageDataUpdated(ImageData data) {}
     190        };
     191        new Expectations(listener) {{
     192            listener.selectedImageChanged(data); times = 2;
     193        }};
     194        data.addImageDataUpdateListener(listener);
     195        data.selectFirstImage();
     196        data.removeSelectedImage();
     197    }
     198
     199    @Test
     200    public void testRemoveImageAndTriggerListener() {
     201        List<ImageEntry> list = this.getOneImage();
     202        ImageData data = new ImageData(list);
     203        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     204            @Override
     205            public void selectedImageChanged(ImageData data) {}
     206
     207            @Override
     208            public void imageDataUpdated(ImageData data) {}
     209        };
     210        new Expectations(listener) {{
     211            listener.imageDataUpdated(data); times = 1;
     212        }};
     213        data.addImageDataUpdateListener(listener);
     214        data.removeImage(list.get(0));
     215        assertEquals(0, data.getImages().size());
     216    }
     217
     218    @Test
     219    public void testMergeFrom() {
     220        ImageEntry image = new ImageEntry(new File("test2"));
     221        List<ImageEntry> list1 = this.getOneImage();
     222        list1.add(image);
     223        List<ImageEntry> list2 = this.getOneImage();
     224        list2.add(new ImageEntry(new File("test3")));
     225
     226        ImageData data = new ImageData(list1);
     227        data.setSelectedImage(list1.get(0));
     228        ImageData data2 = new ImageData(list2);
     229
     230        new MockUp<Collections>() {
     231            @Mock
     232            public void sort(List<ImageEntry> o) {
     233                list1.remove(image);
     234                list1.add(image);
     235            }
     236        };
     237
     238        data.mergeFrom(data2);
     239        assertEquals(3, data.getImages().size());
     240        assertEquals(list1.get(0), data.getSelectedImage());
     241    }
     242
     243    @Test
     244    public void testMergeFromSelectedImage() {
     245        ImageEntry image = new ImageEntry(new File("test2"));
     246        List<ImageEntry> list1 = this.getOneImage();
     247        list1.add(image);
     248        List<ImageEntry> list2 = this.getOneImage();
     249
     250        ImageData data = new ImageData(list1);
     251        ImageData data2 = new ImageData(list2);
     252        data2.setSelectedImage(list2.get(0));
     253
     254        data.mergeFrom(data2);
     255        assertEquals(3, data.getImages().size());
     256        assertEquals(list2.get(0), data.getSelectedImage());
     257    }
     258
     259
     260    @Test
     261    public void testUpdatePosition() {
     262        List<ImageEntry> list = this.getOneImage();
     263        ImageData data = new ImageData(list);
     264
     265        new Expectations(list.get(0)) {{
     266            list.get(0).setPos((LatLon) any);
     267            list.get(0).flagNewGpsData();
     268        }};
     269        data.updateImagePosition(list.get(0), new LatLon(0, 0));
     270    }
     271
     272    @Test
     273    public void testUpdateDirection() {
     274        List<ImageEntry> list = this.getOneImage();
     275        ImageData data = new ImageData(list);
     276
     277        new Expectations(list.get(0)) {{
     278            list.get(0).setExifImgDir(0.0);
     279            list.get(0).flagNewGpsData();
     280        }};
     281        data.updateImageDirection(list.get(0), 0);
     282    }
     283
     284    @Test
     285    public void testTriggerListenerOnUpdate() {
     286        List<ImageEntry> list = this.getOneImage();
     287        ImageData data = new ImageData(list);
     288
     289        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     290            @Override
     291            public void selectedImageChanged(ImageData data) {}
     292
     293            @Override
     294            public void imageDataUpdated(ImageData data) {}
     295        };
     296        new Expectations(listener) {{
     297            listener.imageDataUpdated(data); times = 1;
     298        }};
     299
     300        data.addImageDataUpdateListener(listener);
     301        data.updateImageDirection(list.get(0), 0);
     302    }
     303
     304    @Test
     305    public void testManuallyTriggerUpdateListener() {
     306        List<ImageEntry> list = this.getOneImage();
     307        ImageData data = new ImageData(list);
     308
     309        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     310            @Override
     311            public void selectedImageChanged(ImageData data) {}
     312
     313            @Override
     314            public void imageDataUpdated(ImageData data) {}
     315        };
     316        new Expectations(listener) {{
     317            listener.imageDataUpdated(data); times = 1;
     318        }};
     319
     320        data.addImageDataUpdateListener(listener);
     321        data.notifyImageUpdate();
     322    }
     323}