Ticket #21605: 21605.5.patch

File 21605.5.patch, 52.0 KB (added by taylor.smock, 3 years ago)

Colorize non-selected images in geoimage layers

  • src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java

    Subject: [PATCH] Fix #21605: Add tabs to ImageViewerDialog for use with different image layers
    
    This allows users to have multiple geotagged image layers, and
    quickly switch between them.
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
    a b  
    3333
    3434import javax.swing.Action;
    3535import javax.swing.Icon;
     36import javax.swing.ImageIcon;
    3637
    3738import org.openstreetmap.josm.actions.AutoScaleAction;
    3839import org.openstreetmap.josm.actions.ExpertToggleAction;
     
    4748import org.openstreetmap.josm.data.gpx.GpxData;
    4849import org.openstreetmap.josm.data.gpx.GpxImageEntry;
    4950import org.openstreetmap.josm.data.gpx.GpxTrack;
     51import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    5052import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     53import org.openstreetmap.josm.data.preferences.NamedColorProperty;
    5154import org.openstreetmap.josm.gui.MainApplication;
    5255import org.openstreetmap.josm.gui.MapFrame;
    5356import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
     
    6265import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
    6366import org.openstreetmap.josm.gui.layer.Layer;
    6467import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
     68import org.openstreetmap.josm.gui.util.GuiHelper;
    6569import org.openstreetmap.josm.gui.util.imagery.Vector3D;
    6670import org.openstreetmap.josm.tools.ImageProvider;
     71import org.openstreetmap.josm.tools.ListenerList;
    6772import org.openstreetmap.josm.tools.Utils;
    6873
    6974/**
     
    7176 * @since 99
    7277 */
    7378public class GeoImageLayer extends AbstractModifiableLayer implements
    74         JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener {
     79        JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener,
     80        IGeoImageLayer {
    7581
    7682    private static final List<Action> menuAdditions = new LinkedList<>();
    7783
    7884    private static volatile List<MapMode> supportedMapModes;
    7985
    8086    private final ImageData data;
     87    private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListeners = ListenerList.create();
    8188    GpxData gpxData;
    8289    GpxLayer gpxFauxLayer;
    8390    GpxData gpxFauxData;
     
    8693
    8794    private final Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
    8895    private final Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
     96    private final Icon selectedIconNotImageViewer = generateSelectedIconNotImageViewer(this.selectedIcon);
     97
     98    private static Icon generateSelectedIconNotImageViewer(Icon selectedIcon) {
     99        Color color = new NamedColorProperty("geoimage.selected.not.image.viewer", new Color(255, 170, 255)).get();
     100        BufferedImage bi = new BufferedImage(selectedIcon.getIconWidth(), selectedIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
     101        Graphics2D g2d = bi.createGraphics();
     102        selectedIcon.paintIcon(null, g2d, 0, 0);
     103        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f));
     104        g2d.setColor(color);
     105        g2d.fillRect(0, 0, selectedIcon.getIconWidth(), selectedIcon.getIconHeight());
     106        g2d.dispose();
     107        return new ImageIcon(bi);
     108    }
    89109
    90110    boolean useThumbs;
    91111    private final ExecutorService thumbsLoaderExecutor =
     
    172192        this.useThumbs = useThumbs;
    173193        this.data.addImageDataUpdateListener(this);
    174194        this.data.setLayer(this);
     195        if (!ImageViewerDialog.hasInstance()) {
     196            this.data.setSelectedImage(this.data.getFirstImage());
     197            // This must be called *after* this layer is added to the layer manager.
     198            // But it must also be run in the EDT. By adding this to the worker queue
     199            // and then running the actual code in the EDT, we ensure that the layer
     200            // will be added to the layer manager regardless of whether or not this
     201            // was instantiated in the worker thread or the EDT thread.
     202            MainApplication.worker.submit(() -> GuiHelper.runInEDT(() ->
     203                    ImageViewerDialog.getInstance().displayImages(Collections.singletonList(this.data.getSelectedImage()))));
     204        }
    175205    }
    176206
    177207    private final class ImageMouseListener extends MouseAdapter {
     
    232262                    }
    233263                } else {
    234264                    data.setSelectedImage(img);
    235                     ImageViewerDialog.getInstance().displayImages(GeoImageLayer.this, Collections.singletonList(img));
     265                    ImageViewerDialog.getInstance().displayImages(Collections.singletonList(img));
    236266                }
    237267            }
    238268        }
     
    247277        MainApplication.worker.execute(new ImagesLoader(files, gpxLayer));
    248278    }
    249279
     280    @Override
     281    public void clearSelection() {
     282        this.getImageData().clearSelectedImage();
     283    }
     284
     285    @Override
     286    public boolean containsImage(IImageEntry<?> imageEntry) {
     287        if (imageEntry instanceof ImageEntry) {
     288            return this.data.getImages().contains(imageEntry);
     289        }
     290        return false;
     291    }
     292
    250293    @Override
    251294    public Icon getIcon() {
    252295        return ImageProvider.get("dialogs/geoimage", ImageProvider.ImageSizes.LAYER);
    253296    }
    254297
     298    @Override
     299    public List<ImageEntry> getSelection() {
     300        return this.getImageData().getSelectedImages();
     301    }
     302
     303    @Override
     304    public List<IImageEntry<?>> getInvalidGeoImages() {
     305        return this.getImageData().getImages().stream().filter(entry -> entry.getPos() == null || entry.getExifCoor() == null
     306              || !entry.getExifCoor().isValid() || !entry.getPos().isValid()).collect(toList());
     307    }
     308
     309    @Override
     310    public void addImageChangeListener(ImageChangeListener listener) {
     311        this.imageChangeListeners.addListener(listener);
     312    }
     313
     314    @Override
     315    public void removeImageChangeListener(ImageChangeListener listener) {
     316        this.imageChangeListeners.removeListener(listener);
     317    }
     318
    255319    /**
    256320     * Register actions on the layer
    257321     * @param addition the action to be added
     
    451515            }
    452516        }
    453517
     518        final IImageEntry<?> currentImage = ImageViewerDialog.getCurrentImage();
    454519        for (ImageEntry e: data.getSelectedImages()) {
    455520            if (e != null && e.getPos() != null) {
    456521                Point p = mv.getPoint(e.getPos());
     
    465530                if (useThumbs && e.hasThumbnail()) {
    466531                    g.setColor(new Color(128, 0, 0, 122));
    467532                    g.fillRect(p.x - imgDim.width / 2, p.y - imgDim.height / 2, imgDim.width, imgDim.height);
     533                } else if (e.equals(currentImage)) {
     534                    selectedIcon.paintIcon(mv, g,
     535                            p.x - imgDim.width / 2,
     536                            p.y - imgDim.height / 2);
    468537                } else {
    469                     selectedIcon.paintIcon(mv, g,
     538                    selectedIconNotImageViewer.paintIcon(mv, g,
    470539                            p.x - imgDim.width / 2,
    471540                            p.y - imgDim.height / 2);
    472541                }
     
    884953    @Override
    885954    public void selectedImageChanged(ImageData data) {
    886955        showCurrentPhoto();
     956        this.imageChangeListeners.fireEvent(e -> e.imageChanged(this, null, data.getSelectedImages()));
    887957    }
    888958
    889959    @Override
  • new file src/org/openstreetmap/josm/gui/layer/geoimage/IGeoImageLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/IGeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/IGeoImageLayer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.layer.geoimage;
     3
     4import java.util.Collections;
     5import java.util.List;
     6
     7import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
     8
     9/**
     10 * An interface for layers which want to show images
     11 * @since xxx
     12 */
     13public interface IGeoImageLayer {
     14    /**
     15     * Clear the selection of the layer
     16     */
     17    void clearSelection();
     18
     19    /**
     20     * Get the current selection
     21     * @return The currently selected images
     22     */
     23    List<? extends IImageEntry<?>> getSelection();
     24
     25    /**
     26     * Get the invalid geo images for this layer (specifically, those that <i>cannot</i> be displayed on the map)
     27     * @return The list of invalid geo images
     28     */
     29    default List<IImageEntry<?>> getInvalidGeoImages() {
     30        return Collections.emptyList();
     31    }
     32
     33    /**
     34     * Check if the layer contains the specified image
     35     * @param imageEntry The entry to look for
     36     * @return {@code true} if this layer contains the image
     37     */
     38    boolean containsImage(IImageEntry<?> imageEntry);
     39
     40    /**
     41     * Add a listener for when images change
     42     * @param listener The listener to call
     43     */
     44    void addImageChangeListener(ImageChangeListener listener);
     45
     46    /**
     47     * Remove a listener for when images change
     48     * @param listener The listener to remove
     49     */
     50    void removeImageChangeListener(ImageChangeListener listener);
     51
     52    /**
     53     * Listen for image changes
     54     */
     55    interface ImageChangeListener {
     56        /**
     57         * Called when the selected image(s) change
     58         * @param source The source of the change
     59         * @param oldImages The previously selected image(s)
     60         * @param newImages The newly selected image(s)
     61         */
     62        void imageChanged(IGeoImageLayer source, List<? extends IImageEntry<?>> oldImages, List<? extends IImageEntry<?>> newImages);
     63    }
     64}
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java
    a b  
    147147        if (entry instanceof ImageEntry) {
    148148            this.dataSet.setSelectedImage((ImageEntry) entry);
    149149        }
    150         imageViewerDialog.displayImages(this.dataSet.getLayer(), Collections.singletonList(entry));
     150        imageViewerDialog.displayImages(Collections.singletonList(entry));
    151151    }
    152152
    153153    @Override
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java
    a b  
    1111import java.awt.GridBagConstraints;
    1212import java.awt.GridBagLayout;
    1313import java.awt.event.ActionEvent;
    14 import java.awt.event.ActionListener;
    1514import java.awt.event.KeyEvent;
    1615import java.awt.event.WindowEvent;
    1716import java.io.IOException;
     
    2322import java.util.Arrays;
    2423import java.util.Collections;
    2524import java.util.Comparator;
    26 import java.util.HashMap;
    2725import java.util.List;
    28 import java.util.Map;
    2926import java.util.Objects;
    3027import java.util.Optional;
    3128import java.util.concurrent.Future;
    3229import java.util.function.UnaryOperator;
    3330import java.util.stream.Collectors;
     31import java.util.stream.IntStream;
     32import java.util.stream.Stream;
    3433
    3534import javax.swing.AbstractAction;
     35import javax.swing.AbstractButton;
    3636import javax.swing.Box;
    3737import javax.swing.JButton;
    3838import javax.swing.JLabel;
     
    4242import javax.swing.SwingConstants;
    4343import javax.swing.SwingUtilities;
    4444
     45import org.openstreetmap.josm.actions.ExpertToggleAction;
    4546import org.openstreetmap.josm.actions.JosmAction;
    4647import org.openstreetmap.josm.data.ImageData;
    47 import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
    4848import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    4949import org.openstreetmap.josm.gui.ExtendedDialog;
    5050import org.openstreetmap.josm.gui.MainApplication;
     
    6464import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
    6565import org.openstreetmap.josm.gui.util.GuiHelper;
    6666import org.openstreetmap.josm.gui.util.imagery.Vector3D;
     67import org.openstreetmap.josm.gui.widgets.HideableTabbedPane;
     68import org.openstreetmap.josm.spi.preferences.Config;
    6769import org.openstreetmap.josm.tools.ImageProvider;
    6870import org.openstreetmap.josm.tools.Logging;
    6971import org.openstreetmap.josm.tools.PlatformManager;
     
    7375/**
    7476 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
    7577 */
    76 public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener, ImageDataUpdateListener {
     78public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
    7779    private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}");
    7880    private static final String DIALOG_FOLDER = "dialogs";
    7981
     
    124126        return dialog;
    125127    }
    126128
     129    /**
     130     * Check if there is an instance for the {@link ImageViewerDialog}
     131     * @return {@code true} if there is a static singleton instance of {@link ImageViewerDialog}
     132     * @since xxx
     133     */
     134    public static boolean hasInstance() {
     135        return dialog != null;
     136    }
     137
     138    /**
     139     * Destroy the current dialog
     140     */
     141    private static void destroyInstance() {
     142        dialog = null;
     143    }
     144
    127145    private JButton btnLast;
    128146    private JButton btnNext;
    129147    private JButton btnPrevious;
     
    135153    private JButton btnDeleteFromDisk;
    136154    private JToggleButton tbCentre;
    137155    /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */
    138     private JPanel layers;
     156    private final HideableTabbedPane layers = new HideableTabbedPane();
    139157
    140158    private ImageViewerDialog() {
    141159        super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
     
    168186
    169187    private void build() {
    170188        JPanel content = new JPanel(new BorderLayout());
    171         this.layers = new JPanel(new GridBagLayout());
    172         content.add(layers, BorderLayout.NORTH);
    173 
    174         content.add(imgDisplay, BorderLayout.CENTER);
     189        content.add(this.layers, BorderLayout.CENTER);
    175190
    176191        Dimension buttonDim = new Dimension(26, 26);
    177192
     
    187202        btnLast = createNavigationButton(imageLastAction, buttonDim);
    188203
    189204        tbCentre = new JToggleButton(imageCenterViewAction);
     205        tbCentre.setSelected(Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false));
    190206        tbCentre.setPreferredSize(buttonDim);
    191207
    192208        JButton btnZoomBestFit = new JButton(imageZoomAction);
     
    196212        btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
    197213
    198214        JPanel buttons = new JPanel();
    199         buttons.add(btnFirst);
    200         buttons.add(btnPrevious);
    201         buttons.add(btnNext);
    202         buttons.add(btnLast);
    203         buttons.add(Box.createRigidArea(new Dimension(7, 0)));
    204         buttons.add(tbCentre);
    205         buttons.add(btnZoomBestFit);
    206         buttons.add(Box.createRigidArea(new Dimension(7, 0)));
    207         buttons.add(btnDelete);
    208         buttons.add(btnDeleteFromDisk);
    209         buttons.add(Box.createRigidArea(new Dimension(7, 0)));
    210         buttons.add(btnCopyPath);
    211         buttons.add(btnOpenExternal);
    212         buttons.add(Box.createRigidArea(new Dimension(7, 0)));
    213         buttons.add(createButton(visibilityAction, buttonDim));
     215        addButtonGroup(buttons, this.btnFirst, this.btnPrevious, this.btnNext, this.btnLast);
     216        addButtonGroup(buttons, this.tbCentre, btnZoomBestFit);
     217        addButtonGroup(buttons, this.btnDelete, this.btnDeleteFromDisk);
     218        addButtonGroup(buttons, this.btnCopyPath, this.btnOpenExternal);
     219        addButtonGroup(buttons, createButton(visibilityAction, buttonDim));
    214220
    215221        JPanel bottomPane = new JPanel(new GridBagLayout());
    216222        GridBagConstraints gc = new GridBagConstraints();
     
    231237        createLayout(content, false, null);
    232238    }
    233239
    234     private void updateLayers() {
    235         if (this.tabbedEntries.size() <= 1) {
     240    /**
     241     * Add a button group to a panel
     242     * @param buttonPanel The panel holding the buttons
     243     * @param buttons The button group to add
     244     */
     245    private static void addButtonGroup(JPanel buttonPanel, AbstractButton... buttons) {
     246        if (buttonPanel.getComponentCount() != 0) {
     247            buttonPanel.add(Box.createRigidArea(new Dimension(7, 0)));
     248        }
     249
     250        for (AbstractButton jButton : buttons) {
     251            buttonPanel.add(jButton);
     252        }
     253    }
     254
     255    /**
     256     * Update the tabs for the different image layers
     257     * @param changed {@code true} if the tabs changed
     258     */
     259    private void updateLayers(boolean changed) {
     260        MainLayerManager layerManager = MainApplication.getLayerManager();
     261        List<IGeoImageLayer> geoImageLayers = layerManager.getLayers().stream()
     262                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     263        if (geoImageLayers.isEmpty()) {
    236264            this.layers.setVisible(false);
    237             this.layers.removeAll();
    238265        } else {
    239266            this.layers.setVisible(true);
    240             // Remove all old components
    241             this.layers.removeAll();
    242             MainLayerManager layerManager = MainApplication.getLayerManager();
    243             List<Layer> invalidLayers = this.tabbedEntries.keySet().stream().filter(layer -> !layerManager.containsLayer(layer))
    244                     .collect(Collectors.toList());
    245             // `null` is for anything using the old methods, without telling us what layer it comes from.
    246             invalidLayers.remove(null);
    247             // We need to do multiple calls to avoid ConcurrentModificationExceptions
    248             invalidLayers.forEach(this.tabbedEntries::remove);
    249             addButtonsForImageLayers();
     267            if (changed) {
     268                addButtonsForImageLayers();
     269            }
     270            MoveImgDisplayPanel<?> selected = (MoveImgDisplayPanel<?>) this.layers.getSelectedComponent();
     271            if ((this.imgDisplay.getParent() == null || this.imgDisplay.getParent().getParent() == null)
     272                && selected != null && selected.layer.containsImage(this.currentEntry)) {
     273                selected.setVisible(selected.isVisible());
     274            } else if (selected != null && !selected.layer.containsImage(this.currentEntry)) {
     275                this.getImageTabs().filter(m -> m.layer.containsImage(this.currentEntry)).mapToInt(this.layers::indexOfComponent).findFirst()
     276                        .ifPresent(this.layers::setSelectedIndex);
     277            }
    250278            this.layers.invalidate();
    251279        }
     280        this.layers.getParent().invalidate();
    252281        this.revalidate();
    253282    }
    254283
     
    256285     * Add the buttons for image layers
    257286     */
    258287    private void addButtonsForImageLayers() {
    259         final IImageEntry<?> current;
    260         synchronized (this) {
    261             current = this.currentEntry;
    262         }
    263         List<JButton> layerButtons = new ArrayList<>(this.tabbedEntries.size());
    264         if (this.tabbedEntries.containsKey(null)) {
    265             List<IImageEntry<?>> nullEntries = this.tabbedEntries.get(null);
    266             JButton layerButton = createImageLayerButton(null, nullEntries);
    267             layerButtons.add(layerButton);
    268             layerButton.setEnabled(!nullEntries.contains(current));
     288        List<MoveImgDisplayPanel<?>> alreadyAdded = this.getImageTabs().collect(Collectors.toList());
     289        List<Layer> availableLayers = MainApplication.getLayerManager().getLayers();
     290        for (IGeoImageLayer layer : availableLayers.stream()
     291                .sorted(Comparator.comparingInt(entry -> /*reverse*/-availableLayers.indexOf(entry)))
     292                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList())) {
     293            final int index = availableLayers.size() - availableLayers.indexOf((Layer) layer);
     294            final String label = (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + ((Layer) layer).getLabel();
     295            final Optional<MoveImgDisplayPanel<?>> originalPanel = alreadyAdded.stream()
     296                    .filter(m -> Objects.equals(m.layer, layer)).findFirst();
     297            if (originalPanel.isPresent()) {
     298                int componentIndex = this.layers.indexOfComponent(originalPanel.get());
     299                this.layers.setTitleAt(componentIndex, label);
     300            } else {
     301                this.layers.addTab(label, new MoveImgDisplayPanel<>(this.imgDisplay, (Layer & IGeoImageLayer) layer));
     302            }
    269303        }
    270         for (Map.Entry<Layer, List<IImageEntry<?>>> entry :
    271                 this.tabbedEntries.entrySet().stream().filter(entry -> entry.getKey() != null)
    272                         .sorted(Comparator.comparing(entry -> entry.getKey().getName())).collect(Collectors.toList())) {
    273             JButton layerButton = createImageLayerButton(entry.getKey(), entry.getValue());
    274             layerButtons.add(layerButton);
    275             layerButton.setEnabled(!entry.getValue().contains(current));
     304        this.getImageTabs().map(p -> p.layer).filter(layer -> !availableLayers.contains(layer))
     305                // We have to collect to a list prior to removal -- if we don't, then the stream may get a layer at index 0,
     306                // remove that layer, and then get a layer at index 1, which was previously at index 2.
     307                .collect(Collectors.toList()).forEach(this::removeImageTab);
     308
     309        // This is need to avoid the first button becoming visible, and then recalling this method.
     310        this.getImageTabs().forEach(m -> m.finishedAddingButtons = true);
     311        // After that, trigger the visibility set code
     312        this.getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
     313    }
     314
     315    /**
     316     * Remove a tab for a layer from the {@link #layers} tab pane
     317     * @param layer The layer to remove
     318     */
     319    private void removeImageTab(Layer layer) {
     320        // This must be reversed to avoid removing the wrong tab
     321        for (int i = this.layers.getTabCount() - 1; i >= 0; i--) {
     322            Component component = this.layers.getComponentAt(i);
     323            if (component instanceof MoveImgDisplayPanel) {
     324                MoveImgDisplayPanel<?> moveImgDisplayPanel = (MoveImgDisplayPanel<?>) component;
     325                if (Objects.equals(layer, moveImgDisplayPanel.layer)) {
     326                    this.layers.removeTabAt(i);
     327                    this.layers.remove(moveImgDisplayPanel);
     328                }
     329            }
    276330        }
    277         layerButtons.forEach(this.layers::add);
    278331    }
    279332
    280333    /**
    281      * Create a button for a specific layer and its entries
    282      *
    283      * @param layer     The layer to switch to
    284      * @param entries   The entries to display
    285      * @return The button to use to switch to the specified layer
     334     * Get the {@link MoveImgDisplayPanel} objects in {@link #layers}.
     335     * @return The individual panels
    286336     */
    287     private static JButton createImageLayerButton(Layer layer, List<IImageEntry<?>> entries) {
    288         final JButton layerButton = new JButton();
    289         layerButton.addActionListener(new ImageActionListener(layer, entries));
    290         layerButton.setText(layer != null ? layer.getLabel() : tr("Default"));
    291         return layerButton;
     337    private Stream<MoveImgDisplayPanel<?>> getImageTabs() {
     338        return IntStream.range(0, this.layers.getTabCount())
     339                .mapToObj(this.layers::getComponentAt)
     340                .filter(MoveImgDisplayPanel.class::isInstance)
     341                .map(m -> (MoveImgDisplayPanel<?>) m);
    292342    }
    293343
    294344    @Override
     
    309359        imageZoomAction.destroy();
    310360        cancelLoadingImage();
    311361        super.destroy();
    312         dialog = null;
     362        destroyInstance();
    313363    }
    314364
    315365    /**
     
    433483        }
    434484    }
    435485
    436     /**
    437      * A listener that is called to change the viewing layer
    438      */
    439     private static class ImageActionListener implements ActionListener {
    440 
    441         private final Layer layer;
    442         private final List<IImageEntry<?>> entries;
    443 
    444         ImageActionListener(Layer layer, List<IImageEntry<?>> entries) {
    445             this.layer = layer;
    446             this.entries = entries;
    447         }
    448 
    449         @Override
    450         public void actionPerformed(ActionEvent e) {
    451             ImageViewerDialog.getInstance().displayImages(this.layer, this.entries);
    452         }
    453     }
    454 
    455486    private class ImageFirstAction extends ImageRememberAction {
    456487        ImageFirstAction() {
    457488            super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
     
    478509        public void actionPerformed(ActionEvent e) {
    479510            final JToggleButton button = (JToggleButton) e.getSource();
    480511            centerView = button.isEnabled() && button.isSelected();
     512            Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView);
    481513            if (centerView && currentEntry != null && currentEntry.getPos() != null) {
    482514                MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
    483515            }
     
    618650        }
    619651    }
    620652
     653    /**
     654     * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay arround and (b) setting the imgDisplay as a child
     655     * for this panel.
     656     */
     657    private static class MoveImgDisplayPanel<T extends Layer & IGeoImageLayer> extends JPanel {
     658        private final T layer;
     659        private final ImageDisplay imgDisplay;
     660
     661        /**
     662         * The purpose of this field is to avoid having the same tab added to the dialog multiple times. This is only a problem when the dialog
     663         * has multiple tabs on initialization (like from a session).
     664         */
     665        boolean finishedAddingButtons;
     666        MoveImgDisplayPanel(ImageDisplay imgDisplay, T layer) {
     667            super(new BorderLayout());
     668            this.layer = layer;
     669            this.imgDisplay = imgDisplay;
     670        }
     671
     672        @Override
     673        public void setVisible(boolean visible) {
     674            super.setVisible(visible);
     675            if (visible && this.finishedAddingButtons) {
     676                if (!this.layer.getSelection().isEmpty() && !this.layer.getSelection().contains(ImageViewerDialog.getCurrentImage())) {
     677                    ImageViewerDialog.getInstance().displayImages(this.layer.getSelection());
     678                    this.layer.invalidate();
     679                }
     680                if (this.imgDisplay.getParent() != this) {
     681                    this.add(this.imgDisplay, BorderLayout.CENTER);
     682                    this.imgDisplay.invalidate();
     683                    this.revalidate();
     684                }
     685            }
     686        }
     687    }
     688
    621689    /**
    622690     * Enables (or disables) the "Previous" button.
    623691     * @param value {@code true} to enable the button, {@code false} otherwise
     
    651719        return wasEnabled;
    652720    }
    653721
    654     /** Used for tabbed panes */
    655     private final transient Map<Layer, List<IImageEntry<?>>> tabbedEntries = new HashMap<>();
    656722    private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
    657723
    658724    /**
     
    679745     * @param entries image entries
    680746     * @since 18246
    681747     */
    682     public void displayImages(List<IImageEntry<?>> entries) {
    683         this.displayImages((Layer) null, entries);
    684     }
    685 
    686     /**
    687      * Displays images for the given layer.
    688      * @param layer The layer to use for the tab ui
    689      * @param entries image entries
    690      * @since 18591
    691      */
    692     public void displayImages(Layer layer, List<IImageEntry<?>> entries) {
     748    public void displayImages(List<? extends IImageEntry<?>> entries) {
    693749        boolean imageChanged;
    694750        IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
    695751
     
    710766            }
    711767        }
    712768
    713         if (entries == null || entries.isEmpty() || entries.stream().allMatch(Objects::isNull)) {
    714             this.tabbedEntries.remove(layer);
     769
     770        final boolean updateRequired;
     771        final List<IGeoImageLayer> imageLayers = MainApplication.getLayerManager().getLayers().stream()
     772                    .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     773        if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) {
     774            updateRequired = true;
     775            // Clear the selected images in other geoimage layers
     776            this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast)
     777                    .filter(l -> !Objects.equals(entries, l.getSelection()))
     778                    .forEach(IGeoImageLayer::clearSelection);
    715779        } else {
    716             this.tabbedEntries.put(layer, entries);
     780            updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals));
    717781        }
    718         this.updateLayers();
     782        this.updateLayers(updateRequired);
    719783        if (entry != null) {
    720784            this.updateButtonsNonNullEntry(entry, imageChanged);
    721         } else if (this.tabbedEntries.isEmpty()) {
     785        } else if (imageLayers.isEmpty()) {
    722786            this.updateButtonsNullEntry(entries);
    723787            return;
    724788        } else {
    725             Map.Entry<Layer, List<IImageEntry<?>>> realEntry =
    726                     this.tabbedEntries.entrySet().stream().filter(mapEntry -> mapEntry.getValue().size() == 1).findFirst().orElse(null);
    727             if (realEntry == null) {
     789            IGeoImageLayer layer = imageLayers.stream().filter(l -> l.getSelection().size() == 1).findFirst().orElse(null);
     790            if (layer == null) {
    728791                this.updateButtonsNullEntry(entries);
    729792            } else {
    730                 this.displayImages(realEntry.getKey(), realEntry.getValue());
     793                this.displayImages(layer.getSelection());
    731794            }
    732795            return;
    733796        }
     
    744807     * Update buttons for null entry
    745808     * @param entries {@code true} if multiple images are selected
    746809     */
    747     private void updateButtonsNullEntry(List<IImageEntry<?>> entries) {
     810    private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) {
    748811        boolean hasMultipleImages = entries != null && entries.size() > 1;
    749812        // if this method is called to reinitialize dialog content with a blank image,
    750813        // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
    751         setTitle(tr("Geotagged Images"));
     814        this.updateTitle();
    752815        imgDisplay.setImage(null);
    753816        imgDisplay.setOsdText("");
    754817        setNextEnabled(false);
     
    787850        btnCopyPath.setEnabled(true);
    788851        btnOpenExternal.setEnabled(true);
    789852
    790         setTitle(tr("Geotagged Images") + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
     853        this.updateTitle();
    791854        StringBuilder osd = new StringBuilder(entry.getDisplayName());
    792855        if (entry.getElevation() != null) {
    793856            osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
     
    818881        imgDisplay.setOsdText(osd.toString());
    819882    }
    820883
    821     /**
    822      * Displays images for the given layer.
    823      * @param ignoredData the image data (unused, may be {@code null})
    824      * @param entries image entries
    825      * @since 18246 (signature)
    826      * @deprecated Use {@link #displayImages(List)} (The data param is no longer used)
    827      */
    828     @Deprecated
    829     public void displayImages(ImageData ignoredData, List<IImageEntry<?>> entries) {
    830         this.displayImages(entries);
     884    private void updateTitle() {
     885        final IImageEntry<?> entry;
     886        synchronized (this) {
     887            entry = this.currentEntry;
     888        }
     889        String baseTitle = Optional.of(this.layers.getSelectedComponent())
     890                .filter(MoveImgDisplayPanel.class::isInstance).map(MoveImgDisplayPanel.class::cast)
     891                .map(m -> m.layer).map(Layer::getLabel).orElse(tr("Geotagged Images"));
     892        if (entry == null) {
     893            this.setTitle(baseTitle);
     894        } else {
     895            this.setTitle(baseTitle + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
     896        }
    831897    }
    832898
    833     private static boolean isLastImageSelected(List<IImageEntry<?>> data) {
     899    private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) {
    834900        return data.stream().anyMatch(image -> data.contains(image.getLastImage()));
    835901    }
    836902
    837     private static boolean isFirstImageSelected(List<IImageEntry<?>> data) {
     903    private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) {
    838904        return data.stream().anyMatch(image -> data.contains(image.getFirstImage()));
    839905    }
    840906
     
    857923        if (btnCollapse != null) {
    858924            btnCollapse.setVisible(!isDocked);
    859925        }
    860         this.updateLayers();
     926        this.updateLayers(true);
    861927    }
    862928
    863929    /**
     
    904970
    905971    @Override
    906972    public void layerRemoving(LayerRemoveEvent e) {
    907         if (e.getRemovedLayer() instanceof GeoImageLayer && this.currentEntry instanceof ImageEntry) {
    908             ImageData removedData = ((GeoImageLayer) e.getRemovedLayer()).getImageData();
    909             if (removedData == ((ImageEntry) this.currentEntry).getDataSet()) {
    910                 displayImages(e.getRemovedLayer(), null);
    911             }
    912             removedData.removeImageDataUpdateListener(this);
     973        if (e.getRemovedLayer() instanceof IGeoImageLayer && ((IGeoImageLayer) e.getRemovedLayer()).containsImage(this.currentEntry)) {
     974            displayImages(null);
    913975        }
    914         // Unfortunately, there will be no way to remove the default null layer. This will be fixed as plugins update.
    915         this.tabbedEntries.remove(e.getRemovedLayer());
     976        this.updateLayers(true);
    916977    }
    917978
    918979    @Override
    919980    public void layerOrderChanged(LayerOrderChangeEvent e) {
    920         // ignored
     981        this.updateLayers(true);
    921982    }
    922983
    923984    @Override
     
    9411002    }
    9421003
    9431004    private void registerOnLayer(Layer layer) {
    944         if (layer instanceof GeoImageLayer) {
    945             ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
     1005        if (layer instanceof IGeoImageLayer) {
     1006            layer.addPropertyChangeListener(l -> {
     1007                final List<?> currentTabLayers = this.getImageTabs().map(m -> m.layer).collect(Collectors.toList());
     1008                if (Layer.NAME_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
     1009                    this.updateLayers(true);
     1010                        if (((IGeoImageLayer) layer).containsImage(this.currentEntry)) {
     1011                            this.updateTitle();
     1012                        }
     1013                } else if (Layer.VISIBLE_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
     1014                    this.getImageTabs().filter(m -> Objects.equals(m.layer, layer)).mapToInt(this.layers::indexOfComponent)
     1015                            .filter(i -> i >= 0).forEach(i -> this.layers.setEnabledAt(i, layer.isVisible()));
     1016                }
     1017            });
    9461018        }
    9471019    }
    9481020
     
    9591031            imgLoadingFuture = null;
    9601032        }
    9611033    }
    962 
    963     @Override
    964     public void selectedImageChanged(ImageData data) {
    965         if (this.currentEntry != data.getSelectedImage() && this.currentEntry instanceof ImageEntry &&
    966                 !data.getSelectedImages().contains(this.currentEntry)) {
    967             displayImages(data.getLayer(), new ArrayList<>(data.getSelectedImages()));
    968         }
    969     }
    970 
    971     @Override
    972     public void imageDataUpdated(ImageData data) {
    973         displayImages(data.getLayer(), new ArrayList<>(data.getSelectedImages()));
    974     }
    9751034}
  • src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java b/src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java
    a b  
    1919import org.openstreetmap.josm.data.coor.ILatLon;
    2020import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    2121import org.openstreetmap.josm.data.imagery.street_level.Projections;
    22 import org.openstreetmap.josm.gui.layer.Layer;
    2322import org.openstreetmap.josm.tools.HttpClient;
    2423import org.openstreetmap.josm.tools.JosmRuntimeException;
    2524
     
    3332    private final Supplier<RemoteEntry> nextImage;
    3433    private final Supplier<RemoteEntry> previousImage;
    3534    private final Supplier<RemoteEntry> lastImage;
    36     private final Layer layer;
    3735    private int width;
    3836    private int height;
    3937    private ILatLon pos;
     
    5452
    5553    /**
    5654     * Create a new remote entry
    57      * @param layer The originating layer, used for tabs in the image viewer
    5855     * @param uri The URI to use
    5956     * @param firstImage first image supplier
    6057     * @param nextImage next image supplier
    6158     * @param lastImage last image supplier
    6259     * @param previousImage previous image supplier
    6360     */
    64     public RemoteEntry(Layer layer, URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
     61    public RemoteEntry(URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
    6562                       Supplier<RemoteEntry> nextImage, Supplier<RemoteEntry> lastImage) {
    6663        Objects.requireNonNull(uri);
    6764        Objects.requireNonNull(firstImage);
     
    7370        this.previousImage = previousImage;
    7471        this.nextImage = nextImage;
    7572        this.lastImage = lastImage;
    76         this.layer = layer;
    7773    }
    7874
    7975    @Override
     
    319315
    320316    @Override
    321317    public void selectImage(ImageViewerDialog imageViewerDialog, IImageEntry<?> entry) {
    322         imageViewerDialog.displayImages(this.layer, Collections.singletonList(entry));
     318        imageViewerDialog.displayImages(Collections.singletonList(entry));
    323319    }
    324320
    325321    @Override
  • src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java b/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java
    a b  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.awt.event.ActionEvent;
     7import java.net.URI;
    78import java.net.URISyntaxException;
    89import java.net.URL;
    910import java.time.Instant;
     
    1617import org.openstreetmap.josm.data.gpx.GpxConstants;
    1718import org.openstreetmap.josm.data.gpx.GpxLink;
    1819import org.openstreetmap.josm.data.gpx.WayPoint;
     20import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    1921import org.openstreetmap.josm.gui.Notification;
    2022import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog;
    2123import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry;
     
    4042
    4143    @Override
    4244    public void actionPerformed(ActionEvent ev) {
    43         ImageViewerDialog.getInstance().displayImages(this.parentLayer, Collections.singletonList(getRemoteEntry()));
     45        this.parentLayer.setCurrentMarker(this);
     46        ImageViewerDialog.getInstance().displayImages(Collections.singletonList(getRemoteEntry()));
    4447    }
    4548
    46     private RemoteEntry getRemoteEntry() {
     49    RemoteEntry getRemoteEntry() {
    4750        try {
    48             final RemoteEntry remoteEntry = new RemoteEntry(this.parentLayer, imageUrl.toURI(), getFirstImage(), getPreviousImage(),
     51            final RemoteEntry remoteEntry = new MarkerRemoteEntry(imageUrl.toURI(), getFirstImage(), getPreviousImage(),
    4952                    getNextImage(), getLastImage());
    5053            // First, extract EXIF data
    5154            remoteEntry.extractExif();
     
    128131        wpt.put(GpxConstants.META_LINKS, Collections.singleton(link));
    129132        return wpt;
    130133    }
     134
     135    private class MarkerRemoteEntry extends RemoteEntry {
     136        /**
     137         * Create a new remote entry
     138         *
     139         * @param uri           The URI to use
     140         * @param firstImage    first image supplier
     141         * @param previousImage previous image supplier
     142         * @param nextImage     next image supplier
     143         * @param lastImage     last image supplier
     144         */
     145        MarkerRemoteEntry(URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
     146                                 Supplier<RemoteEntry> nextImage, Supplier<RemoteEntry> lastImage) {
     147            super(uri, firstImage, previousImage, nextImage, lastImage);
     148        }
     149
     150        @Override
     151        public void selectImage(ImageViewerDialog imageViewerDialog, IImageEntry<?> entry) {
     152            ImageMarker.this.parentLayer.setCurrentMarker(ImageMarker.this);
     153            super.selectImage(imageViewerDialog, entry);
     154        }
     155    }
    131156}
  • src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java b/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java
    a b  
    2020
    2121import javax.swing.ImageIcon;
    2222
     23import org.openstreetmap.josm.data.IQuadBucketType;
    2324import org.openstreetmap.josm.data.Preferences;
    2425import org.openstreetmap.josm.data.coor.CachedLatLon;
    2526import org.openstreetmap.josm.data.coor.EastNorth;
     
    2728import org.openstreetmap.josm.data.coor.LatLon;
    2829import org.openstreetmap.josm.data.gpx.GpxConstants;
    2930import org.openstreetmap.josm.data.gpx.WayPoint;
     31import org.openstreetmap.josm.data.osm.BBox;
    3032import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
    3133import org.openstreetmap.josm.gui.MapView;
    3234import org.openstreetmap.josm.gui.layer.GpxLayer;
     
    7577 *
    7678 * @author Frederik Ramm
    7779 */
    78 public class Marker implements TemplateEngineDataProvider, ILatLon, Destroyable {
     80public class Marker implements TemplateEngineDataProvider, ILatLon, Destroyable, IQuadBucketType {
    7981
    8082    /**
    8183     * Plugins can add their Marker creation stuff at the bottom or top of this list
     
    447449    private String getPreferenceKey() {
    448450        return "draw.rawgps." + getTextTemplateKey();
    449451    }
     452
     453    @Override
     454    public BBox getBBox() {
     455        return new BBox(this);
     456    }
    450457}
  • src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java b/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java
    a b  
    1919import java.net.URISyntaxException;
    2020import java.util.ArrayList;
    2121import java.util.Collection;
     22import java.util.Collections;
    2223import java.util.Comparator;
    2324import java.util.HashMap;
    2425import java.util.List;
     26import java.util.ListIterator;
    2527import java.util.Map;
     28import java.util.Objects;
    2629import java.util.Optional;
    2730
    2831import javax.swing.AbstractAction;
     
    4144import org.openstreetmap.josm.data.gpx.GpxLink;
    4245import org.openstreetmap.josm.data.gpx.IGpxLayerPrefs;
    4346import org.openstreetmap.josm.data.gpx.WayPoint;
     47import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
     48import org.openstreetmap.josm.data.osm.BBox;
     49import org.openstreetmap.josm.data.osm.QuadBuckets;
    4450import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    4551import org.openstreetmap.josm.data.preferences.IntegerProperty;
    4652import org.openstreetmap.josm.data.preferences.NamedColorProperty;
     
    5561import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
    5662import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
    5763import org.openstreetmap.josm.gui.layer.Layer;
     64import org.openstreetmap.josm.gui.layer.geoimage.IGeoImageLayer;
     65import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry;
    5866import org.openstreetmap.josm.gui.layer.gpx.ConvertFromMarkerLayerAction;
    5967import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
    6068import org.openstreetmap.josm.io.audio.AudioPlayer;
    6169import org.openstreetmap.josm.spi.preferences.Config;
    6270import org.openstreetmap.josm.tools.ColorHelper;
    6371import org.openstreetmap.josm.tools.ImageProvider;
     72import org.openstreetmap.josm.tools.ListenerList;
    6473import org.openstreetmap.josm.tools.Logging;
    6574import org.openstreetmap.josm.tools.Utils;
    6675
     
    7584 *
    7685 * The data is read only.
    7786 */
    78 public class MarkerLayer extends Layer implements JumpToMarkerLayer {
     87public class MarkerLayer extends Layer implements JumpToMarkerLayer, IGeoImageLayer {
    7988
    8089    /**
    8190     * A list of markers.
     
    8998    final int markerSize = new IntegerProperty("draw.rawgps.markers.size", 4).get();
    9099    final BasicStroke markerStroke = new StrokeProperty("draw.rawgps.markers.stroke", "1").get();
    91100
     101    private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListenerListenerList = ListenerList.create();
     102
    92103    /**
    93104     * The default color that is used for drawing markers.
    94105     */
     
    401412        MainApplication.getMap().mapView.zoomTo(currentMarker);
    402413    }
    403414
     415    /**
     416     * Set the current marker
     417     * @param newMarker The marker to set
     418     */
     419    void setCurrentMarker(Marker newMarker) {
     420        this.currentMarker = newMarker;
     421    }
     422
    404423    public static void playAudio() {
    405424        playAdjacentMarker(null, true);
    406425    }
     
    495514        this.realcolor = Optional.ofNullable(color).orElse(DEFAULT_COLOR_PROPERTY.get());
    496515    }
    497516
     517    @Override
     518    public void clearSelection() {
     519        this.currentMarker = null;
     520    }
     521
     522    @Override
     523    public List<? extends IImageEntry<?>> getSelection() {
     524        if (this.currentMarker instanceof ImageMarker) {
     525            return Collections.singletonList(((ImageMarker) this.currentMarker).getRemoteEntry());
     526        }
     527        return Collections.emptyList();
     528    }
     529
     530    @Override
     531    public boolean containsImage(IImageEntry<?> imageEntry) {
     532        if (imageEntry instanceof RemoteEntry) {
     533            RemoteEntry entry = (RemoteEntry) imageEntry;
     534            if (entry.getPos() != null && entry.getPos().isLatLonKnown()) {
     535                List<Marker> markers = this.data.search(new BBox(entry.getPos()));
     536                return checkIfListContainsEntry(markers, entry);
     537            } else if (entry.getExifCoor() != null && entry.getExifCoor().isLatLonKnown()) {
     538                List<Marker> markers = this.data.search(new BBox(entry.getExifCoor()));
     539                return checkIfListContainsEntry(markers, entry);
     540            } else {
     541                return checkIfListContainsEntry(this.data, entry);
     542            }
     543        }
     544        return false;
     545    }
     546
     547    /**
     548     * Check if a list contains an entry
     549     * @param markerList The list to look through
     550     * @param imageEntry The image entry to check
     551     * @return {@code true} if the entry is in the list
     552     */
     553    private static boolean checkIfListContainsEntry(List<Marker> markerList, RemoteEntry imageEntry) {
     554        for (Marker marker : markerList) {
     555            if (marker instanceof ImageMarker) {
     556                ImageMarker imageMarker = (ImageMarker) marker;
     557                try {
     558                    if (Objects.equals(imageMarker.imageUrl.toURI(), imageEntry.getImageURI())) {
     559                        return true;
     560                    }
     561                } catch (URISyntaxException e) {
     562                    Logging.trace(e);
     563                }
     564            }
     565        }
     566        return false;
     567    }
     568
     569    @Override
     570    public void addImageChangeListener(ImageChangeListener listener) {
     571        this.imageChangeListenerListenerList.addListener(listener);
     572    }
     573
     574    @Override
     575    public void removeImageChangeListener(ImageChangeListener listener) {
     576        this.imageChangeListenerListenerList.removeListener(listener);
     577    }
     578
    498579    private final class MarkerMouseAdapter extends MouseAdapter {
    499580        @Override
    500581        public void mousePressed(MouseEvent e) {
     
    627708     * the data of a MarkerLayer
    628709     * @since 18287
    629710     */
    630     public class MarkerData extends ArrayList<Marker> implements IGpxLayerPrefs {
     711    public class MarkerData extends QuadBuckets<Marker> implements List<Marker>, IGpxLayerPrefs {
    631712
    632713        private Map<String, String> ownLayerPrefs;
     714        private final List<Marker> markerList = new ArrayList<>();
    633715
    634716        @Override
    635717        public Map<String, String> getLayerPrefs() {
     
    658740                fromLayer.data.setModified(value);
    659741            }
    660742        }
     743
     744        @Override
     745        public boolean addAll(int index, Collection<? extends Marker> c) {
     746            c.forEach(this::add);
     747            return this.markerList.addAll(index, c);
     748        }
     749
     750        @Override
     751        public boolean addAll(Collection<? extends Marker> objects) {
     752            return this.markerList.addAll(objects) && super.addAll(objects);
     753        }
     754
     755        @Override
     756        public Marker get(int index) {
     757            return this.markerList.get(index);
     758        }
     759
     760        @Override
     761        public Marker set(int index, Marker element) {
     762            Marker original = this.markerList.set(index, element);
     763            this.remove(original);
     764            return original;
     765        }
     766
     767        @Override
     768        public void add(int index, Marker element) {
     769            this.add(element);
     770            this.markerList.add(index, element);
     771        }
     772
     773        @Override
     774        public Marker remove(int index) {
     775            Marker toRemove = this.markerList.remove(index);
     776            this.remove(toRemove);
     777            return toRemove;
     778        }
     779
     780        @Override
     781        public int indexOf(Object o) {
     782            return this.markerList.indexOf(o);
     783        }
     784
     785        @Override
     786        public int lastIndexOf(Object o) {
     787            return this.markerList.lastIndexOf(o);
     788        }
     789
     790        @Override
     791        public ListIterator<Marker> listIterator() {
     792            return this.markerList.listIterator();
     793        }
     794
     795        @Override
     796        public ListIterator<Marker> listIterator(int index) {
     797            return this.markerList.listIterator(index);
     798        }
     799
     800        @Override
     801        public List<Marker> subList(int fromIndex, int toIndex) {
     802            return this.markerList.subList(fromIndex, toIndex);
     803        }
     804
     805        @Override
     806        public boolean retainAll(Collection<?> objects) {
     807            return this.markerList.retainAll(objects) && super.retainAll(objects);
     808        }
     809
     810        @Override
     811        public boolean contains(Object o) {
     812            return this.markerList.contains(o) && super.contains(o);
     813        }
    661814    }
    662815}