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.
---
Index: src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
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/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java	(revision 18607)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java	(date 1669907957599)
@@ -33,6 +33,7 @@
 
 import javax.swing.Action;
 import javax.swing.Icon;
+import javax.swing.ImageIcon;
 
 import org.openstreetmap.josm.actions.AutoScaleAction;
 import org.openstreetmap.josm.actions.ExpertToggleAction;
@@ -47,7 +48,9 @@
 import org.openstreetmap.josm.data.gpx.GpxData;
 import org.openstreetmap.josm.data.gpx.GpxImageEntry;
 import org.openstreetmap.josm.data.gpx.GpxTrack;
+import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.preferences.NamedColorProperty;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MapFrame;
 import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
@@ -62,8 +65,10 @@
 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
 import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
+import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.imagery.Vector3D;
 import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ListenerList;
 import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -71,13 +76,15 @@
  * @since 99
  */
 public class GeoImageLayer extends AbstractModifiableLayer implements
-        JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener {
+        JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener,
+        IGeoImageLayer {
 
     private static final List<Action> menuAdditions = new LinkedList<>();
 
     private static volatile List<MapMode> supportedMapModes;
 
     private final ImageData data;
+    private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListeners = ListenerList.create();
     GpxData gpxData;
     GpxLayer gpxFauxLayer;
     GpxData gpxFauxData;
@@ -86,6 +93,19 @@
 
     private final Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
     private final Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
+    private final Icon selectedIconNotImageViewer = generateSelectedIconNotImageViewer(this.selectedIcon);
+
+    private static Icon generateSelectedIconNotImageViewer(Icon selectedIcon) {
+        Color color = new NamedColorProperty("geoimage.selected.not.image.viewer", new Color(50, 0, 0)).get();
+        BufferedImage bi = new BufferedImage(selectedIcon.getIconWidth(), selectedIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
+        Graphics2D g2d = bi.createGraphics();
+        selectedIcon.paintIcon(null, g2d, 0, 0);
+        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f));
+        g2d.setColor(color);
+        g2d.fillRect(0, 0, selectedIcon.getIconWidth(), selectedIcon.getIconHeight());
+        g2d.dispose();
+        return new ImageIcon(bi);
+    }
 
     boolean useThumbs;
     private final ExecutorService thumbsLoaderExecutor =
@@ -172,6 +192,14 @@
         this.useThumbs = useThumbs;
         this.data.addImageDataUpdateListener(this);
         this.data.setLayer(this);
+        synchronized (ImageViewerDialog.class) {
+            if (!ImageViewerDialog.hasInstance()) {
+                GuiHelper.runInEDTAndWait(ImageViewerDialog::createInstance);
+            }
+        }
+        if (getInvalidGeoImages().size() == data.size()) {
+            ImageViewerDialog.getInstance().displayImages(Collections.singletonList(this.data.getFirstImage()));
+        }
     }
 
     private final class ImageMouseListener extends MouseAdapter {
@@ -232,8 +260,9 @@
                     }
                 } else {
                     data.setSelectedImage(img);
-                    ImageViewerDialog.getInstance().displayImages(GeoImageLayer.this, Collections.singletonList(img));
+                    ImageViewerDialog.getInstance().displayImages(Collections.singletonList(img));
                 }
+                GeoImageLayer.this.invalidate(); // Needed to update which image is being shown in the image viewer in the mapview
             }
         }
     }
@@ -247,11 +276,45 @@
         MainApplication.worker.execute(new ImagesLoader(files, gpxLayer));
     }
 
+    @Override
+    public void clearSelection() {
+        this.getImageData().clearSelectedImage();
+    }
+
+    @Override
+    public boolean containsImage(IImageEntry<?> imageEntry) {
+        if (imageEntry instanceof ImageEntry) {
+            return this.data.getImages().contains(imageEntry);
+        }
+        return false;
+    }
+
     @Override
     public Icon getIcon() {
         return ImageProvider.get("dialogs/geoimage", ImageProvider.ImageSizes.LAYER);
     }
 
+    @Override
+    public List<ImageEntry> getSelection() {
+        return this.getImageData().getSelectedImages();
+    }
+
+    @Override
+    public List<IImageEntry<?>> getInvalidGeoImages() {
+        return this.getImageData().getImages().stream().filter(entry -> entry.getPos() == null || entry.getExifCoor() == null
+              || !entry.getExifCoor().isValid() || !entry.getPos().isValid()).collect(toList());
+    }
+
+    @Override
+    public void addImageChangeListener(ImageChangeListener listener) {
+        this.imageChangeListeners.addListener(listener);
+    }
+
+    @Override
+    public void removeImageChangeListener(ImageChangeListener listener) {
+        this.imageChangeListeners.removeListener(listener);
+    }
+
     /**
      * Register actions on the layer
      * @param addition the action to be added
@@ -451,6 +514,7 @@
             }
         }
 
+        final IImageEntry<?> currentImage = ImageViewerDialog.getCurrentImage();
         for (ImageEntry e: data.getSelectedImages()) {
             if (e != null && e.getPos() != null) {
                 Point p = mv.getPoint(e.getPos());
@@ -465,8 +529,12 @@
                 if (useThumbs && e.hasThumbnail()) {
                     g.setColor(new Color(128, 0, 0, 122));
                     g.fillRect(p.x - imgDim.width / 2, p.y - imgDim.height / 2, imgDim.width, imgDim.height);
+                } else if (e.equals(currentImage)) {
+                    selectedIcon.paintIcon(mv, g,
+                            p.x - imgDim.width / 2,
+                            p.y - imgDim.height / 2);
                 } else {
-                    selectedIcon.paintIcon(mv, g,
+                    selectedIconNotImageViewer.paintIcon(mv, g,
                             p.x - imgDim.width / 2,
                             p.y - imgDim.height / 2);
                 }
@@ -884,6 +952,7 @@
     @Override
     public void selectedImageChanged(ImageData data) {
         showCurrentPhoto();
+        this.imageChangeListeners.fireEvent(e -> e.imageChanged(this, null, data.getSelectedImages()));
     }
 
     @Override
Index: 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
--- /dev/null	(date 1669819623212)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/IGeoImageLayer.java	(date 1669819623212)
@@ -0,0 +1,64 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.layer.geoimage;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
+
+/**
+ * An interface for layers which want to show images
+ * @since xxx
+ */
+public interface IGeoImageLayer {
+    /**
+     * Clear the selection of the layer
+     */
+    void clearSelection();
+
+    /**
+     * Get the current selection
+     * @return The currently selected images
+     */
+    List<? extends IImageEntry<?>> getSelection();
+
+    /**
+     * Get the invalid geo images for this layer (specifically, those that <i>cannot</i> be displayed on the map)
+     * @return The list of invalid geo images
+     */
+    default List<IImageEntry<?>> getInvalidGeoImages() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Check if the layer contains the specified image
+     * @param imageEntry The entry to look for
+     * @return {@code true} if this layer contains the image
+     */
+    boolean containsImage(IImageEntry<?> imageEntry);
+
+    /**
+     * Add a listener for when images change
+     * @param listener The listener to call
+     */
+    void addImageChangeListener(ImageChangeListener listener);
+
+    /**
+     * Remove a listener for when images change
+     * @param listener The listener to remove
+     */
+    void removeImageChangeListener(ImageChangeListener listener);
+
+    /**
+     * Listen for image changes
+     */
+    interface ImageChangeListener {
+        /**
+         * Called when the selected image(s) change
+         * @param source The source of the change
+         * @param oldImages The previously selected image(s)
+         * @param newImages The newly selected image(s)
+         */
+        void imageChanged(IGeoImageLayer source, List<? extends IImageEntry<?>> oldImages, List<? extends IImageEntry<?>> newImages);
+    }
+}
Index: 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/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(revision 18607)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageEntry.java	(date 1669820958178)
@@ -147,7 +147,7 @@
         if (entry instanceof ImageEntry) {
             this.dataSet.setSelectedImage((ImageEntry) entry);
         }
-        imageViewerDialog.displayImages(this.dataSet.getLayer(), Collections.singletonList(entry));
+        imageViewerDialog.displayImages(Collections.singletonList(entry));
     }
 
     @Override
Index: 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/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java	(revision 18607)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java	(date 1670262533800)
@@ -14,6 +14,8 @@
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
 import java.awt.event.WindowEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import java.io.IOException;
 import java.io.Serializable;
 import java.time.ZoneOffset;
@@ -23,28 +25,31 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.Future;
 import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 import javax.swing.AbstractAction;
+import javax.swing.AbstractButton;
+import javax.swing.BorderFactory;
 import javax.swing.Box;
 import javax.swing.JButton;
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
 import javax.swing.JToggleButton;
 import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
 
+import org.openstreetmap.josm.actions.ExpertToggleAction;
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.data.ImageData;
-import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -64,6 +69,8 @@
 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.util.imagery.Vector3D;
+import org.openstreetmap.josm.gui.widgets.HideableTabbedPane;
+import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.PlatformManager;
@@ -73,7 +80,7 @@
 /**
  * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
  */
-public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener, ImageDataUpdateListener {
+public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
     private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}");
     private static final String DIALOG_FOLDER = "dialogs";
 
@@ -124,6 +131,22 @@
         return dialog;
     }
 
+    /**
+     * Check if there is an instance for the {@link ImageViewerDialog}
+     * @return {@code true} if there is a static singleton instance of {@link ImageViewerDialog}
+     * @since xxx
+     */
+    public static boolean hasInstance() {
+        return dialog != null;
+    }
+
+    /**
+     * Destroy the current dialog
+     */
+    private static void destroyInstance() {
+        dialog = null;
+    }
+
     private JButton btnLast;
     private JButton btnNext;
     private JButton btnPrevious;
@@ -135,7 +158,7 @@
     private JButton btnDeleteFromDisk;
     private JToggleButton tbCentre;
     /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */
-    private JPanel layers;
+    private final HideableTabbedPane layers = new HideableTabbedPane();
 
     private ImageViewerDialog() {
         super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
@@ -168,10 +191,7 @@
 
     private void build() {
         JPanel content = new JPanel(new BorderLayout());
-        this.layers = new JPanel(new GridBagLayout());
-        content.add(layers, BorderLayout.NORTH);
-
-        content.add(imgDisplay, BorderLayout.CENTER);
+        content.add(this.layers, BorderLayout.CENTER);
 
         Dimension buttonDim = new Dimension(26, 26);
 
@@ -187,6 +207,7 @@
         btnLast = createNavigationButton(imageLastAction, buttonDim);
 
         tbCentre = new JToggleButton(imageCenterViewAction);
+        tbCentre.setSelected(Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false));
         tbCentre.setPreferredSize(buttonDim);
 
         JButton btnZoomBestFit = new JButton(imageZoomAction);
@@ -196,21 +217,11 @@
         btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
 
         JPanel buttons = new JPanel();
-        buttons.add(btnFirst);
-        buttons.add(btnPrevious);
-        buttons.add(btnNext);
-        buttons.add(btnLast);
-        buttons.add(Box.createRigidArea(new Dimension(7, 0)));
-        buttons.add(tbCentre);
-        buttons.add(btnZoomBestFit);
-        buttons.add(Box.createRigidArea(new Dimension(7, 0)));
-        buttons.add(btnDelete);
-        buttons.add(btnDeleteFromDisk);
-        buttons.add(Box.createRigidArea(new Dimension(7, 0)));
-        buttons.add(btnCopyPath);
-        buttons.add(btnOpenExternal);
-        buttons.add(Box.createRigidArea(new Dimension(7, 0)));
-        buttons.add(createButton(visibilityAction, buttonDim));
+        addButtonGroup(buttons, this.btnFirst, this.btnPrevious, this.btnNext, this.btnLast);
+        addButtonGroup(buttons, this.tbCentre, btnZoomBestFit);
+        addButtonGroup(buttons, this.btnDelete, this.btnDeleteFromDisk);
+        addButtonGroup(buttons, this.btnCopyPath, this.btnOpenExternal);
+        addButtonGroup(buttons, createButton(visibilityAction, buttonDim));
 
         JPanel bottomPane = new JPanel(new GridBagLayout());
         GridBagConstraints gc = new GridBagConstraints();
@@ -231,24 +242,47 @@
         createLayout(content, false, null);
     }
 
-    private void updateLayers() {
-        if (this.tabbedEntries.size() <= 1) {
+    /**
+     * Add a button group to a panel
+     * @param buttonPanel The panel holding the buttons
+     * @param buttons The button group to add
+     */
+    private static void addButtonGroup(JPanel buttonPanel, AbstractButton... buttons) {
+        if (buttonPanel.getComponentCount() != 0) {
+            buttonPanel.add(Box.createRigidArea(new Dimension(7, 0)));
+        }
+
+        for (AbstractButton jButton : buttons) {
+            buttonPanel.add(jButton);
+        }
+    }
+
+    /**
+     * Update the tabs for the different image layers
+     * @param changed {@code true} if the tabs changed
+     */
+    private void updateLayers(boolean changed) {
+        MainLayerManager layerManager = MainApplication.getLayerManager();
+        List<IGeoImageLayer> geoImageLayers = layerManager.getLayers().stream()
+                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
+        if (geoImageLayers.isEmpty()) {
             this.layers.setVisible(false);
-            this.layers.removeAll();
         } else {
             this.layers.setVisible(true);
-            // Remove all old components
-            this.layers.removeAll();
-            MainLayerManager layerManager = MainApplication.getLayerManager();
-            List<Layer> invalidLayers = this.tabbedEntries.keySet().stream().filter(layer -> !layerManager.containsLayer(layer))
-                    .collect(Collectors.toList());
-            // `null` is for anything using the old methods, without telling us what layer it comes from.
-            invalidLayers.remove(null);
-            // We need to do multiple calls to avoid ConcurrentModificationExceptions
-            invalidLayers.forEach(this.tabbedEntries::remove);
-            addButtonsForImageLayers();
+            if (changed) {
+                addButtonsForImageLayers();
+            }
+            MoveImgDisplayPanel<?> selected = (MoveImgDisplayPanel<?>) this.layers.getSelectedComponent();
+            if ((this.imgDisplay.getParent() == null || this.imgDisplay.getParent().getParent() == null)
+                && selected != null && selected.layer.containsImage(this.currentEntry)) {
+                selected.setVisible(selected.isVisible());
+            } else if (selected != null && !selected.layer.containsImage(this.currentEntry)) {
+                this.getImageTabs().filter(m -> m.layer.containsImage(this.currentEntry)).mapToInt(this.layers::indexOfComponent).findFirst()
+                        .ifPresent(this.layers::setSelectedIndex);
+            }
             this.layers.invalidate();
         }
+        this.layers.getParent().invalidate();
         this.revalidate();
     }
 
@@ -256,39 +290,88 @@
      * Add the buttons for image layers
      */
     private void addButtonsForImageLayers() {
-        final IImageEntry<?> current;
-        synchronized (this) {
-            current = this.currentEntry;
-        }
-        List<JButton> layerButtons = new ArrayList<>(this.tabbedEntries.size());
-        if (this.tabbedEntries.containsKey(null)) {
-            List<IImageEntry<?>> nullEntries = this.tabbedEntries.get(null);
-            JButton layerButton = createImageLayerButton(null, nullEntries);
-            layerButtons.add(layerButton);
-            layerButton.setEnabled(!nullEntries.contains(current));
+        List<MoveImgDisplayPanel<?>> alreadyAdded = this.getImageTabs().collect(Collectors.toList());
+        // Avoid the setVisible call recursively calling this method and adding duplicates
+        alreadyAdded.forEach(m -> m.finishedAddingButtons = false);
+        List<Layer> availableLayers = MainApplication.getLayerManager().getLayers();
+        List<IGeoImageLayer> geoImageLayers = availableLayers.stream()
+                .sorted(Comparator.comparingInt(entry -> /*reverse*/-availableLayers.indexOf(entry)))
+                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
+        List<IGeoImageLayer> tabLayers = geoImageLayers.stream()
+                .filter(l -> alreadyAdded.stream().anyMatch(m -> Objects.equals(l, m.layer)) || l.containsImage(this.currentEntry))
+                .collect(Collectors.toList());
+        for (IGeoImageLayer layer : tabLayers) {
+            final MoveImgDisplayPanel<?> panel = alreadyAdded.stream()
+                    .filter(m -> Objects.equals(m.layer, layer)).findFirst()
+                    .orElseGet(() -> new MoveImgDisplayPanel<>(this.imgDisplay, (Layer & IGeoImageLayer) layer));
+            int componentIndex = this.layers.indexOfComponent(panel);
+            if (componentIndex == geoImageLayers.indexOf(layer)) {
+                this.layers.setTitleAt(componentIndex, panel.getLabel(availableLayers));
+            } else {
+                this.removeImageTab((Layer) layer);
+                this.layers.insertTab(panel.getLabel(availableLayers), null, panel, null, tabLayers.indexOf(layer));
+                int idx = this.layers.indexOfComponent(panel);
+                CloseableTab closeableTab = new CloseableTab(l -> {
+                    Component source = (Component) l.getSource();
+                    int index = layers.indexOfTabComponent(source);
+                    while (index < 0 && source != null) {
+                        index = layers.indexOfTabComponent(source);
+                        source = source.getParent();
+                        if (index >= 0) {
+                            getImageTabs().forEach(m -> m.finishedAddingButtons = false);
+                            removeImageTab(((MoveImgDisplayPanel<?>) layers.getComponentAt(index)).layer);
+                            getImageTabs().forEach(m -> m.finishedAddingButtons = true);
+                            getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
+                            layers.revalidate();
+                            return;
+                        }
+                    }
+                });
+                this.layers.addPropertyChangeListener("indexForTitle", closeableTab);
+                this.layers.setTabComponentAt(idx, closeableTab);
+            }
+            if (layer.containsImage(this.currentEntry)) {
+                this.layers.setSelectedComponent(panel);
+            }
         }
-        for (Map.Entry<Layer, List<IImageEntry<?>>> entry :
-                this.tabbedEntries.entrySet().stream().filter(entry -> entry.getKey() != null)
-                        .sorted(Comparator.comparing(entry -> entry.getKey().getName())).collect(Collectors.toList())) {
-            JButton layerButton = createImageLayerButton(entry.getKey(), entry.getValue());
-            layerButtons.add(layerButton);
-            layerButton.setEnabled(!entry.getValue().contains(current));
+        this.getImageTabs().map(p -> p.layer).filter(layer -> !availableLayers.contains(layer))
+                // We have to collect to a list prior to removal -- if we don't, then the stream may get a layer at index 0,
+                // remove that layer, and then get a layer at index 1, which was previously at index 2.
+                .collect(Collectors.toList()).forEach(this::removeImageTab);
+
+        // This is need to avoid the first button becoming visible, and then recalling this method.
+        this.getImageTabs().forEach(m -> m.finishedAddingButtons = true);
+        // After that, trigger the visibility set code
+        this.getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
+    }
+
+    /**
+     * Remove a tab for a layer from the {@link #layers} tab pane
+     * @param layer The layer to remove
+     */
+    private void removeImageTab(Layer layer) {
+        // This must be reversed to avoid removing the wrong tab
+        for (int i = this.layers.getTabCount() - 1; i >= 0; i--) {
+            Component component = this.layers.getComponentAt(i);
+            if (component instanceof MoveImgDisplayPanel) {
+                MoveImgDisplayPanel<?> moveImgDisplayPanel = (MoveImgDisplayPanel<?>) component;
+                if (Objects.equals(layer, moveImgDisplayPanel.layer)) {
+                    this.layers.removeTabAt(i);
+                    this.layers.remove(moveImgDisplayPanel);
+                }
+            }
         }
-        layerButtons.forEach(this.layers::add);
     }
 
     /**
-     * Create a button for a specific layer and its entries
-     *
-     * @param layer     The layer to switch to
-     * @param entries   The entries to display
-     * @return The button to use to switch to the specified layer
+     * Get the {@link MoveImgDisplayPanel} objects in {@link #layers}.
+     * @return The individual panels
      */
-    private static JButton createImageLayerButton(Layer layer, List<IImageEntry<?>> entries) {
-        final JButton layerButton = new JButton();
-        layerButton.addActionListener(new ImageActionListener(layer, entries));
-        layerButton.setText(layer != null ? layer.getLabel() : tr("Default"));
-        return layerButton;
+    private Stream<MoveImgDisplayPanel<?>> getImageTabs() {
+        return IntStream.range(0, this.layers.getTabCount())
+                .mapToObj(this.layers::getComponentAt)
+                .filter(MoveImgDisplayPanel.class::isInstance)
+                .map(m -> (MoveImgDisplayPanel<?>) m);
     }
 
     @Override
@@ -309,7 +392,7 @@
         imageZoomAction.destroy();
         cancelLoadingImage();
         super.destroy();
-        dialog = null;
+        destroyInstance();
     }
 
     /**
@@ -433,25 +516,6 @@
         }
     }
 
-    /**
-     * A listener that is called to change the viewing layer
-     */
-    private static class ImageActionListener implements ActionListener {
-
-        private final Layer layer;
-        private final List<IImageEntry<?>> entries;
-
-        ImageActionListener(Layer layer, List<IImageEntry<?>> entries) {
-            this.layer = layer;
-            this.entries = entries;
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            ImageViewerDialog.getInstance().displayImages(this.layer, this.entries);
-        }
-    }
-
     private class ImageFirstAction extends ImageRememberAction {
         ImageFirstAction() {
             super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
@@ -478,6 +542,7 @@
         public void actionPerformed(ActionEvent e) {
             final JToggleButton button = (JToggleButton) e.getSource();
             centerView = button.isEnabled() && button.isSelected();
+            Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView);
             if (centerView && currentEntry != null && currentEntry.getPos() != null) {
                 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
             }
@@ -618,6 +683,93 @@
         }
     }
 
+    /**
+     * A tab title renderer for {@link HideableTabbedPane} that allows us to close tabs.
+     * It should be added to the listeners for {@code "indexForTitle"} property changes.
+     * See {@link HideableTabbedPane#addPropertyChangeListener(String, PropertyChangeListener)}.
+     */
+    private static class CloseableTab extends JPanel implements PropertyChangeListener {
+        private final JLabel title;
+
+        /**
+         * Create a new {@link CloseableTab}.
+         * @param closeAction The action to run to close the tab. You probably want to call {@link JTabbedPane#removeTabAt(int)}
+         *                    at the very least.
+         */
+        CloseableTab(ActionListener closeAction) {
+            this.title = new JLabel();
+            this.add(this.title);
+            JButton close = new JButton(ImageProvider.get("misc", "close"));
+            close.setBorder(BorderFactory.createEmptyBorder());
+            this.add(close);
+            close.addActionListener(closeAction);
+        }
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            if ("indexForTitle".equals(evt.getPropertyName()) && evt.getSource() instanceof JTabbedPane) {
+                JTabbedPane source = (JTabbedPane) evt.getSource();
+                int idx = source.indexOfTabComponent(this);
+                if (idx >= 0) {
+                    this.title.setText(source.getTitleAt(idx));
+                }
+            }
+        }
+    }
+
+    /**
+     * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay arround and (b) setting the imgDisplay as a child
+     * for this panel.
+     */
+    private static class MoveImgDisplayPanel<T extends Layer & IGeoImageLayer> extends JPanel {
+        private final T layer;
+        private final ImageDisplay imgDisplay;
+
+        /**
+         * 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
+         * has multiple tabs on initialization (like from a session).
+         */
+        boolean finishedAddingButtons;
+        MoveImgDisplayPanel(ImageDisplay imgDisplay, T layer) {
+            super(new BorderLayout());
+            this.layer = layer;
+            this.imgDisplay = imgDisplay;
+        }
+
+        @Override
+        public void setVisible(boolean visible) {
+            super.setVisible(visible);
+            JTabbedPane layers = ImageViewerDialog.getInstance().layers;
+            int index = layers.indexOfComponent(this);
+            if (visible && this.finishedAddingButtons) {
+                if (!this.layer.getSelection().isEmpty() && !this.layer.getSelection().contains(ImageViewerDialog.getCurrentImage())) {
+                    ImageViewerDialog.getInstance().displayImages(this.layer.getSelection());
+                    this.layer.invalidate(); // This will force the geoimage layers to update properly.
+                }
+                if (this.imgDisplay.getParent() != this) {
+                    this.add(this.imgDisplay, BorderLayout.CENTER);
+                    this.imgDisplay.invalidate();
+                    this.revalidate();
+                }
+                if (index >= 0) {
+                    layers.setTitleAt(index, "* " + getLabel(MainApplication.getLayerManager().getLayers()));
+                }
+            } else if (index >= 0) {
+                layers.setTitleAt(index, getLabel(MainApplication.getLayerManager().getLayers()));
+            }
+        }
+
+        /**
+         * Get the label for this panel
+         * @param availableLayers The layers to use to get the index
+         * @return The label for this layer
+         */
+        String getLabel(List<Layer> availableLayers) {
+            final int index = availableLayers.size() - availableLayers.indexOf(layer);
+            return (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + layer.getLabel();
+        }
+    }
+
     /**
      * Enables (or disables) the "Previous" button.
      * @param value {@code true} to enable the button, {@code false} otherwise
@@ -651,8 +803,6 @@
         return wasEnabled;
     }
 
-    /** Used for tabbed panes */
-    private final transient Map<Layer, List<IImageEntry<?>>> tabbedEntries = new HashMap<>();
     private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
 
     /**
@@ -679,17 +829,7 @@
      * @param entries image entries
      * @since 18246
      */
-    public void displayImages(List<IImageEntry<?>> entries) {
-        this.displayImages((Layer) null, entries);
-    }
-
-    /**
-     * Displays images for the given layer.
-     * @param layer The layer to use for the tab ui
-     * @param entries image entries
-     * @since 18591
-     */
-    public void displayImages(Layer layer, List<IImageEntry<?>> entries) {
+    public void displayImages(List<? extends IImageEntry<?>> entries) {
         boolean imageChanged;
         IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
 
@@ -710,24 +850,31 @@
             }
         }
 
-        if (entries == null || entries.isEmpty() || entries.stream().allMatch(Objects::isNull)) {
-            this.tabbedEntries.remove(layer);
+
+        final boolean updateRequired;
+        final List<IGeoImageLayer> imageLayers = MainApplication.getLayerManager().getLayers().stream()
+                    .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
+        if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) {
+            updateRequired = true;
+            // Clear the selected images in other geoimage layers
+            this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast)
+                    .filter(l -> !Objects.equals(entries, l.getSelection()))
+                    .forEach(IGeoImageLayer::clearSelection);
         } else {
-            this.tabbedEntries.put(layer, entries);
+            updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals));
         }
-        this.updateLayers();
+        this.updateLayers(updateRequired);
         if (entry != null) {
             this.updateButtonsNonNullEntry(entry, imageChanged);
-        } else if (this.tabbedEntries.isEmpty()) {
+        } else if (imageLayers.isEmpty()) {
             this.updateButtonsNullEntry(entries);
             return;
         } else {
-            Map.Entry<Layer, List<IImageEntry<?>>> realEntry =
-                    this.tabbedEntries.entrySet().stream().filter(mapEntry -> mapEntry.getValue().size() == 1).findFirst().orElse(null);
-            if (realEntry == null) {
+            IGeoImageLayer layer = this.getImageTabs().map(m -> m.layer).filter(l -> l.getSelection().size() == 1).findFirst().orElse(null);
+            if (layer == null) {
                 this.updateButtonsNullEntry(entries);
             } else {
-                this.displayImages(realEntry.getKey(), realEntry.getValue());
+                this.displayImages(layer.getSelection());
             }
             return;
         }
@@ -744,11 +891,11 @@
      * Update buttons for null entry
      * @param entries {@code true} if multiple images are selected
      */
-    private void updateButtonsNullEntry(List<IImageEntry<?>> entries) {
+    private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) {
         boolean hasMultipleImages = entries != null && entries.size() > 1;
         // if this method is called to reinitialize dialog content with a blank image,
         // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
-        setTitle(tr("Geotagged Images"));
+        this.updateTitle();
         imgDisplay.setImage(null);
         imgDisplay.setOsdText("");
         setNextEnabled(false);
@@ -787,7 +934,7 @@
         btnCopyPath.setEnabled(true);
         btnOpenExternal.setEnabled(true);
 
-        setTitle(tr("Geotagged Images") + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
+        this.updateTitle();
         StringBuilder osd = new StringBuilder(entry.getDisplayName());
         if (entry.getElevation() != null) {
             osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
@@ -818,23 +965,26 @@
         imgDisplay.setOsdText(osd.toString());
     }
 
-    /**
-     * Displays images for the given layer.
-     * @param ignoredData the image data (unused, may be {@code null})
-     * @param entries image entries
-     * @since 18246 (signature)
-     * @deprecated Use {@link #displayImages(List)} (The data param is no longer used)
-     */
-    @Deprecated
-    public void displayImages(ImageData ignoredData, List<IImageEntry<?>> entries) {
-        this.displayImages(entries);
+    private void updateTitle() {
+        final IImageEntry<?> entry;
+        synchronized (this) {
+            entry = this.currentEntry;
+        }
+        String baseTitle = Optional.ofNullable(this.layers.getSelectedComponent())
+                .filter(MoveImgDisplayPanel.class::isInstance).map(MoveImgDisplayPanel.class::cast)
+                .map(m -> m.layer).map(Layer::getLabel).orElse(tr("Geotagged Images"));
+        if (entry == null) {
+            this.setTitle(baseTitle);
+        } else {
+            this.setTitle(baseTitle + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
+        }
     }
 
-    private static boolean isLastImageSelected(List<IImageEntry<?>> data) {
+    private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) {
         return data.stream().anyMatch(image -> data.contains(image.getLastImage()));
     }
 
-    private static boolean isFirstImageSelected(List<IImageEntry<?>> data) {
+    private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) {
         return data.stream().anyMatch(image -> data.contains(image.getFirstImage()));
     }
 
@@ -857,7 +1007,7 @@
         if (btnCollapse != null) {
             btnCollapse.setVisible(!isDocked);
         }
-        this.updateLayers();
+        this.updateLayers(true);
     }
 
     /**
@@ -904,20 +1054,15 @@
 
     @Override
     public void layerRemoving(LayerRemoveEvent e) {
-        if (e.getRemovedLayer() instanceof GeoImageLayer && this.currentEntry instanceof ImageEntry) {
-            ImageData removedData = ((GeoImageLayer) e.getRemovedLayer()).getImageData();
-            if (removedData == ((ImageEntry) this.currentEntry).getDataSet()) {
-                displayImages(e.getRemovedLayer(), null);
-            }
-            removedData.removeImageDataUpdateListener(this);
+        if (e.getRemovedLayer() instanceof IGeoImageLayer && ((IGeoImageLayer) e.getRemovedLayer()).containsImage(this.currentEntry)) {
+            displayImages(null);
         }
-        // Unfortunately, there will be no way to remove the default null layer. This will be fixed as plugins update.
-        this.tabbedEntries.remove(e.getRemovedLayer());
+        this.updateLayers(true);
     }
 
     @Override
     public void layerOrderChanged(LayerOrderChangeEvent e) {
-        // ignored
+        this.updateLayers(true);
     }
 
     @Override
@@ -941,8 +1086,16 @@
     }
 
     private void registerOnLayer(Layer layer) {
-        if (layer instanceof GeoImageLayer) {
-            ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
+        if (layer instanceof IGeoImageLayer) {
+            layer.addPropertyChangeListener(l -> {
+                final List<?> currentTabLayers = this.getImageTabs().map(m -> m.layer).collect(Collectors.toList());
+                if (Layer.NAME_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
+                    this.updateLayers(true);
+                        if (((IGeoImageLayer) layer).containsImage(this.currentEntry)) {
+                            this.updateTitle();
+                        }
+                } // Use Layer.VISIBLE_PROP here if we decide to do something when layer visibility changes
+            });
         }
     }
 
@@ -951,6 +1104,9 @@
             ImageData imageData = ((GeoImageLayer) newLayer).getImageData();
             imageData.setSelectedImage(imageData.getFirstImage());
         }
+        if (newLayer instanceof IGeoImageLayer) {
+            this.updateLayers(true);
+        }
     }
 
     private void cancelLoadingImage() {
@@ -959,17 +1115,4 @@
             imgLoadingFuture = null;
         }
     }
-
-    @Override
-    public void selectedImageChanged(ImageData data) {
-        if (this.currentEntry != data.getSelectedImage() && this.currentEntry instanceof ImageEntry &&
-                !data.getSelectedImages().contains(this.currentEntry)) {
-            displayImages(data.getLayer(), new ArrayList<>(data.getSelectedImages()));
-        }
-    }
-
-    @Override
-    public void imageDataUpdated(ImageData data) {
-        displayImages(data.getLayer(), new ArrayList<>(data.getSelectedImages()));
-    }
 }
Index: 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/src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java	(revision 18607)
+++ b/src/org/openstreetmap/josm/gui/layer/geoimage/RemoteEntry.java	(date 1669823934542)
@@ -19,7 +19,6 @@
 import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
 import org.openstreetmap.josm.data.imagery.street_level.Projections;
-import org.openstreetmap.josm.gui.layer.Layer;
 import org.openstreetmap.josm.tools.HttpClient;
 import org.openstreetmap.josm.tools.JosmRuntimeException;
 
@@ -33,7 +32,6 @@
     private final Supplier<RemoteEntry> nextImage;
     private final Supplier<RemoteEntry> previousImage;
     private final Supplier<RemoteEntry> lastImage;
-    private final Layer layer;
     private int width;
     private int height;
     private ILatLon pos;
@@ -54,14 +52,13 @@
 
     /**
      * Create a new remote entry
-     * @param layer The originating layer, used for tabs in the image viewer
      * @param uri The URI to use
      * @param firstImage first image supplier
      * @param nextImage next image supplier
      * @param lastImage last image supplier
      * @param previousImage previous image supplier
      */
-    public RemoteEntry(Layer layer, URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
+    public RemoteEntry(URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
                        Supplier<RemoteEntry> nextImage, Supplier<RemoteEntry> lastImage) {
         Objects.requireNonNull(uri);
         Objects.requireNonNull(firstImage);
@@ -73,7 +70,6 @@
         this.previousImage = previousImage;
         this.nextImage = nextImage;
         this.lastImage = lastImage;
-        this.layer = layer;
     }
 
     @Override
@@ -319,7 +315,7 @@
 
     @Override
     public void selectImage(ImageViewerDialog imageViewerDialog, IImageEntry<?> entry) {
-        imageViewerDialog.displayImages(this.layer, Collections.singletonList(entry));
+        imageViewerDialog.displayImages(Collections.singletonList(entry));
     }
 
     @Override
Index: 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/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(revision 18607)
+++ b/src/org/openstreetmap/josm/gui/layer/markerlayer/ImageMarker.java	(date 1669825948452)
@@ -4,6 +4,7 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.event.ActionEvent;
+import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.time.Instant;
@@ -16,6 +17,7 @@
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.GpxLink;
 import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
 import org.openstreetmap.josm.gui.Notification;
 import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog;
 import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry;
@@ -40,12 +42,13 @@
 
     @Override
     public void actionPerformed(ActionEvent ev) {
-        ImageViewerDialog.getInstance().displayImages(this.parentLayer, Collections.singletonList(getRemoteEntry()));
+        this.parentLayer.setCurrentMarker(this);
+        ImageViewerDialog.getInstance().displayImages(Collections.singletonList(getRemoteEntry()));
     }
 
-    private RemoteEntry getRemoteEntry() {
+    RemoteEntry getRemoteEntry() {
         try {
-            final RemoteEntry remoteEntry = new RemoteEntry(this.parentLayer, imageUrl.toURI(), getFirstImage(), getPreviousImage(),
+            final RemoteEntry remoteEntry = new MarkerRemoteEntry(imageUrl.toURI(), getFirstImage(), getPreviousImage(),
                     getNextImage(), getLastImage());
             // First, extract EXIF data
             remoteEntry.extractExif();
@@ -128,4 +131,26 @@
         wpt.put(GpxConstants.META_LINKS, Collections.singleton(link));
         return wpt;
     }
+
+    private class MarkerRemoteEntry extends RemoteEntry {
+        /**
+         * Create a new remote entry
+         *
+         * @param uri           The URI to use
+         * @param firstImage    first image supplier
+         * @param previousImage previous image supplier
+         * @param nextImage     next image supplier
+         * @param lastImage     last image supplier
+         */
+        MarkerRemoteEntry(URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,
+                                 Supplier<RemoteEntry> nextImage, Supplier<RemoteEntry> lastImage) {
+            super(uri, firstImage, previousImage, nextImage, lastImage);
+        }
+
+        @Override
+        public void selectImage(ImageViewerDialog imageViewerDialog, IImageEntry<?> entry) {
+            ImageMarker.this.parentLayer.setCurrentMarker(ImageMarker.this);
+            super.selectImage(imageViewerDialog, entry);
+        }
+    }
 }
Index: 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/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java	(revision 18607)
+++ b/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java	(date 1669821415630)
@@ -20,6 +20,7 @@
 
 import javax.swing.ImageIcon;
 
+import org.openstreetmap.josm.data.IQuadBucketType;
 import org.openstreetmap.josm.data.Preferences;
 import org.openstreetmap.josm.data.coor.CachedLatLon;
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -27,6 +28,7 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.data.osm.BBox;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.layer.GpxLayer;
@@ -75,7 +77,7 @@
  *
  * @author Frederik Ramm
  */
-public class Marker implements TemplateEngineDataProvider, ILatLon, Destroyable {
+public class Marker implements TemplateEngineDataProvider, ILatLon, Destroyable, IQuadBucketType {
 
     /**
      * Plugins can add their Marker creation stuff at the bottom or top of this list
@@ -447,4 +449,9 @@
     private String getPreferenceKey() {
         return "draw.rawgps." + getTextTemplateKey();
     }
+
+    @Override
+    public BBox getBBox() {
+        return new BBox(this);
+    }
 }
Index: 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/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java	(revision 18607)
+++ b/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java	(date 1669825187976)
@@ -19,10 +19,13 @@
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 
 import javax.swing.AbstractAction;
@@ -41,6 +44,9 @@
 import org.openstreetmap.josm.data.gpx.GpxLink;
 import org.openstreetmap.josm.data.gpx.IGpxLayerPrefs;
 import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
+import org.openstreetmap.josm.data.osm.BBox;
+import org.openstreetmap.josm.data.osm.QuadBuckets;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
 import org.openstreetmap.josm.data.preferences.NamedColorProperty;
@@ -55,12 +61,15 @@
 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker;
 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
 import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.geoimage.IGeoImageLayer;
+import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry;
 import org.openstreetmap.josm.gui.layer.gpx.ConvertFromMarkerLayerAction;
 import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
 import org.openstreetmap.josm.io.audio.AudioPlayer;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.ColorHelper;
 import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.ListenerList;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -75,7 +84,7 @@
  *
  * The data is read only.
  */
-public class MarkerLayer extends Layer implements JumpToMarkerLayer {
+public class MarkerLayer extends Layer implements JumpToMarkerLayer, IGeoImageLayer {
 
     /**
      * A list of markers.
@@ -89,6 +98,8 @@
     final int markerSize = new IntegerProperty("draw.rawgps.markers.size", 4).get();
     final BasicStroke markerStroke = new StrokeProperty("draw.rawgps.markers.stroke", "1").get();
 
+    private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListenerListenerList = ListenerList.create();
+
     /**
      * The default color that is used for drawing markers.
      */
@@ -401,6 +412,14 @@
         MainApplication.getMap().mapView.zoomTo(currentMarker);
     }
 
+    /**
+     * Set the current marker
+     * @param newMarker The marker to set
+     */
+    void setCurrentMarker(Marker newMarker) {
+        this.currentMarker = newMarker;
+    }
+
     public static void playAudio() {
         playAdjacentMarker(null, true);
     }
@@ -495,6 +514,68 @@
         this.realcolor = Optional.ofNullable(color).orElse(DEFAULT_COLOR_PROPERTY.get());
     }
 
+    @Override
+    public void clearSelection() {
+        this.currentMarker = null;
+    }
+
+    @Override
+    public List<? extends IImageEntry<?>> getSelection() {
+        if (this.currentMarker instanceof ImageMarker) {
+            return Collections.singletonList(((ImageMarker) this.currentMarker).getRemoteEntry());
+        }
+        return Collections.emptyList();
+    }
+
+    @Override
+    public boolean containsImage(IImageEntry<?> imageEntry) {
+        if (imageEntry instanceof RemoteEntry) {
+            RemoteEntry entry = (RemoteEntry) imageEntry;
+            if (entry.getPos() != null && entry.getPos().isLatLonKnown()) {
+                List<Marker> markers = this.data.search(new BBox(entry.getPos()));
+                return checkIfListContainsEntry(markers, entry);
+            } else if (entry.getExifCoor() != null && entry.getExifCoor().isLatLonKnown()) {
+                List<Marker> markers = this.data.search(new BBox(entry.getExifCoor()));
+                return checkIfListContainsEntry(markers, entry);
+            } else {
+                return checkIfListContainsEntry(this.data, entry);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Check if a list contains an entry
+     * @param markerList The list to look through
+     * @param imageEntry The image entry to check
+     * @return {@code true} if the entry is in the list
+     */
+    private static boolean checkIfListContainsEntry(List<Marker> markerList, RemoteEntry imageEntry) {
+        for (Marker marker : markerList) {
+            if (marker instanceof ImageMarker) {
+                ImageMarker imageMarker = (ImageMarker) marker;
+                try {
+                    if (Objects.equals(imageMarker.imageUrl.toURI(), imageEntry.getImageURI())) {
+                        return true;
+                    }
+                } catch (URISyntaxException e) {
+                    Logging.trace(e);
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void addImageChangeListener(ImageChangeListener listener) {
+        this.imageChangeListenerListenerList.addListener(listener);
+    }
+
+    @Override
+    public void removeImageChangeListener(ImageChangeListener listener) {
+        this.imageChangeListenerListenerList.removeListener(listener);
+    }
+
     private final class MarkerMouseAdapter extends MouseAdapter {
         @Override
         public void mousePressed(MouseEvent e) {
@@ -627,9 +708,10 @@
      * the data of a MarkerLayer
      * @since 18287
      */
-    public class MarkerData extends ArrayList<Marker> implements IGpxLayerPrefs {
+    public class MarkerData extends QuadBuckets<Marker> implements List<Marker>, IGpxLayerPrefs {
 
         private Map<String, String> ownLayerPrefs;
+        private final List<Marker> markerList = new ArrayList<>();
 
         @Override
         public Map<String, String> getLayerPrefs() {
@@ -658,5 +740,76 @@
                 fromLayer.data.setModified(value);
             }
         }
+
+        @Override
+        public boolean addAll(int index, Collection<? extends Marker> c) {
+            c.forEach(this::add);
+            return this.markerList.addAll(index, c);
+        }
+
+        @Override
+        public boolean addAll(Collection<? extends Marker> objects) {
+            return this.markerList.addAll(objects) && super.addAll(objects);
+        }
+
+        @Override
+        public Marker get(int index) {
+            return this.markerList.get(index);
+        }
+
+        @Override
+        public Marker set(int index, Marker element) {
+            Marker original = this.markerList.set(index, element);
+            this.remove(original);
+            return original;
+        }
+
+        @Override
+        public void add(int index, Marker element) {
+            this.add(element);
+            this.markerList.add(index, element);
+        }
+
+        @Override
+        public Marker remove(int index) {
+            Marker toRemove = this.markerList.remove(index);
+            this.remove(toRemove);
+            return toRemove;
+        }
+
+        @Override
+        public int indexOf(Object o) {
+            return this.markerList.indexOf(o);
+        }
+
+        @Override
+        public int lastIndexOf(Object o) {
+            return this.markerList.lastIndexOf(o);
+        }
+
+        @Override
+        public ListIterator<Marker> listIterator() {
+            return this.markerList.listIterator();
+        }
+
+        @Override
+        public ListIterator<Marker> listIterator(int index) {
+            return this.markerList.listIterator(index);
+        }
+
+        @Override
+        public List<Marker> subList(int fromIndex, int toIndex) {
+            return this.markerList.subList(fromIndex, toIndex);
+        }
+
+        @Override
+        public boolean retainAll(Collection<?> objects) {
+            return this.markerList.retainAll(objects) && super.retainAll(objects);
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return this.markerList.contains(o) && super.contains(o);
+        }
     }
 }
