Ticket #21605: 21605.6.patch

File 21605.6.patch, 53.0 KB (added by taylor.smock, 3 years ago)
  • 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;
     
    6467import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
    6568import org.openstreetmap.josm.gui.util.imagery.Vector3D;
    6669import org.openstreetmap.josm.tools.ImageProvider;
     70import org.openstreetmap.josm.tools.ListenerList;
    6771import org.openstreetmap.josm.tools.Utils;
    6872
    6973/**
     
    7175 * @since 99
    7276 */
    7377public class GeoImageLayer extends AbstractModifiableLayer implements
    74         JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener {
     78        JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener,
     79        IGeoImageLayer {
    7580
    7681    private static final List<Action> menuAdditions = new LinkedList<>();
    7782
    7883    private static volatile List<MapMode> supportedMapModes;
    7984
    8085    private final ImageData data;
     86    private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListeners = ListenerList.create();
    8187    GpxData gpxData;
    8288    GpxLayer gpxFauxLayer;
    8389    GpxData gpxFauxData;
     
    8692
    8793    private final Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
    8894    private final Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
     95    private final Icon selectedIconNotImageViewer = generateSelectedIconNotImageViewer(this.selectedIcon);
     96
     97    private static Icon generateSelectedIconNotImageViewer(Icon selectedIcon) {
     98        Color color = new NamedColorProperty("geoimage.selected.not.image.viewer", new Color(50, 0, 0)).get();
     99        BufferedImage bi = new BufferedImage(selectedIcon.getIconWidth(), selectedIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
     100        Graphics2D g2d = bi.createGraphics();
     101        selectedIcon.paintIcon(null, g2d, 0, 0);
     102        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f));
     103        g2d.setColor(color);
     104        g2d.fillRect(0, 0, selectedIcon.getIconWidth(), selectedIcon.getIconHeight());
     105        g2d.dispose();
     106        return new ImageIcon(bi);
     107    }
    89108
    90109    boolean useThumbs;
    91110    private final ExecutorService thumbsLoaderExecutor =
     
    172191        this.useThumbs = useThumbs;
    173192        this.data.addImageDataUpdateListener(this);
    174193        this.data.setLayer(this);
     194        synchronized (ImageViewerDialog.class) {
     195            if (!ImageViewerDialog.hasInstance()) {
     196                ImageViewerDialog.createInstance();
     197            }
     198        }
     199        if (getInvalidGeoImages().size() == data.size()) {
     200            ImageViewerDialog.getInstance().displayImages(Collections.singletonList(this.data.getFirstImage()));
     201        }
    175202    }
    176203
    177204    private final class ImageMouseListener extends MouseAdapter {
     
    232259                    }
    233260                } else {
    234261                    data.setSelectedImage(img);
    235                     ImageViewerDialog.getInstance().displayImages(GeoImageLayer.this, Collections.singletonList(img));
     262                    ImageViewerDialog.getInstance().displayImages(Collections.singletonList(img));
    236263                }
     264                GeoImageLayer.this.invalidate(); // Needed to update which image is being shown in the image viewer in the mapview
    237265            }
    238266        }
    239267    }
     
    247275        MainApplication.worker.execute(new ImagesLoader(files, gpxLayer));
    248276    }
    249277
     278    @Override
     279    public void clearSelection() {
     280        this.getImageData().clearSelectedImage();
     281    }
     282
     283    @Override
     284    public boolean containsImage(IImageEntry<?> imageEntry) {
     285        if (imageEntry instanceof ImageEntry) {
     286            return this.data.getImages().contains(imageEntry);
     287        }
     288        return false;
     289    }
     290
    250291    @Override
    251292    public Icon getIcon() {
    252293        return ImageProvider.get("dialogs/geoimage", ImageProvider.ImageSizes.LAYER);
    253294    }
    254295
     296    @Override
     297    public List<ImageEntry> getSelection() {
     298        return this.getImageData().getSelectedImages();
     299    }
     300
     301    @Override
     302    public List<IImageEntry<?>> getInvalidGeoImages() {
     303        return this.getImageData().getImages().stream().filter(entry -> entry.getPos() == null || entry.getExifCoor() == null
     304              || !entry.getExifCoor().isValid() || !entry.getPos().isValid()).collect(toList());
     305    }
     306
     307    @Override
     308    public void addImageChangeListener(ImageChangeListener listener) {
     309        this.imageChangeListeners.addListener(listener);
     310    }
     311
     312    @Override
     313    public void removeImageChangeListener(ImageChangeListener listener) {
     314        this.imageChangeListeners.removeListener(listener);
     315    }
     316
    255317    /**
    256318     * Register actions on the layer
    257319     * @param addition the action to be added
     
    451513            }
    452514        }
    453515
     516        final IImageEntry<?> currentImage = ImageViewerDialog.getCurrentImage();
    454517        for (ImageEntry e: data.getSelectedImages()) {
    455518            if (e != null && e.getPos() != null) {
    456519                Point p = mv.getPoint(e.getPos());
     
    465528                if (useThumbs && e.hasThumbnail()) {
    466529                    g.setColor(new Color(128, 0, 0, 122));
    467530                    g.fillRect(p.x - imgDim.width / 2, p.y - imgDim.height / 2, imgDim.width, imgDim.height);
     531                } else if (e.equals(currentImage)) {
     532                    selectedIcon.paintIcon(mv, g,
     533                            p.x - imgDim.width / 2,
     534                            p.y - imgDim.height / 2);
    468535                } else {
    469                     selectedIcon.paintIcon(mv, g,
     536                    selectedIconNotImageViewer.paintIcon(mv, g,
    470537                            p.x - imgDim.width / 2,
    471538                            p.y - imgDim.height / 2);
    472539                }
     
    884951    @Override
    885952    public void selectedImageChanged(ImageData data) {
    886953        showCurrentPhoto();
     954        this.imageChangeListeners.fireEvent(e -> e.imageChanged(this, null, data.getSelectedImages()));
    887955    }
    888956
    889957    @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;
    3939import javax.swing.JOptionPane;
    4040import javax.swing.JPanel;
     41import javax.swing.JTabbedPane;
    4142import javax.swing.JToggleButton;
    4243import javax.swing.SwingConstants;
    4344import javax.swing.SwingUtilities;
    4445
     46import org.openstreetmap.josm.actions.ExpertToggleAction;
    4547import org.openstreetmap.josm.actions.JosmAction;
    4648import org.openstreetmap.josm.data.ImageData;
    47 import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
    4849import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    4950import org.openstreetmap.josm.gui.ExtendedDialog;
    5051import org.openstreetmap.josm.gui.MainApplication;
     
    6465import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
    6566import org.openstreetmap.josm.gui.util.GuiHelper;
    6667import org.openstreetmap.josm.gui.util.imagery.Vector3D;
     68import org.openstreetmap.josm.gui.widgets.HideableTabbedPane;
     69import org.openstreetmap.josm.spi.preferences.Config;
    6770import org.openstreetmap.josm.tools.ImageProvider;
    6871import org.openstreetmap.josm.tools.Logging;
    6972import org.openstreetmap.josm.tools.PlatformManager;
     
    7376/**
    7477 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
    7578 */
    76 public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener, ImageDataUpdateListener {
     79public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
    7780    private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}");
    7881    private static final String DIALOG_FOLDER = "dialogs";
    7982
     
    124127        return dialog;
    125128    }
    126129
     130    /**
     131     * Check if there is an instance for the {@link ImageViewerDialog}
     132     * @return {@code true} if there is a static singleton instance of {@link ImageViewerDialog}
     133     * @since xxx
     134     */
     135    public static boolean hasInstance() {
     136        return dialog != null;
     137    }
     138
     139    /**
     140     * Destroy the current dialog
     141     */
     142    private static void destroyInstance() {
     143        dialog = null;
     144    }
     145
    127146    private JButton btnLast;
    128147    private JButton btnNext;
    129148    private JButton btnPrevious;
     
    135154    private JButton btnDeleteFromDisk;
    136155    private JToggleButton tbCentre;
    137156    /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */
    138     private JPanel layers;
     157    private final HideableTabbedPane layers = new HideableTabbedPane();
    139158
    140159    private ImageViewerDialog() {
    141160        super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
     
    168187
    169188    private void build() {
    170189        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);
     190        content.add(this.layers, BorderLayout.CENTER);
    175191
    176192        Dimension buttonDim = new Dimension(26, 26);
    177193
     
    187203        btnLast = createNavigationButton(imageLastAction, buttonDim);
    188204
    189205        tbCentre = new JToggleButton(imageCenterViewAction);
     206        tbCentre.setSelected(Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false));
    190207        tbCentre.setPreferredSize(buttonDim);
    191208
    192209        JButton btnZoomBestFit = new JButton(imageZoomAction);
     
    196213        btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
    197214
    198215        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));
     216        addButtonGroup(buttons, this.btnFirst, this.btnPrevious, this.btnNext, this.btnLast);
     217        addButtonGroup(buttons, this.tbCentre, btnZoomBestFit);
     218        addButtonGroup(buttons, this.btnDelete, this.btnDeleteFromDisk);
     219        addButtonGroup(buttons, this.btnCopyPath, this.btnOpenExternal);
     220        addButtonGroup(buttons, createButton(visibilityAction, buttonDim));
    214221
    215222        JPanel bottomPane = new JPanel(new GridBagLayout());
    216223        GridBagConstraints gc = new GridBagConstraints();
     
    231238        createLayout(content, false, null);
    232239    }
    233240
    234     private void updateLayers() {
    235         if (this.tabbedEntries.size() <= 1) {
     241    /**
     242     * Add a button group to a panel
     243     * @param buttonPanel The panel holding the buttons
     244     * @param buttons The button group to add
     245     */
     246    private static void addButtonGroup(JPanel buttonPanel, AbstractButton... buttons) {
     247        if (buttonPanel.getComponentCount() != 0) {
     248            buttonPanel.add(Box.createRigidArea(new Dimension(7, 0)));
     249        }
     250
     251        for (AbstractButton jButton : buttons) {
     252            buttonPanel.add(jButton);
     253        }
     254    }
     255
     256    /**
     257     * Update the tabs for the different image layers
     258     * @param changed {@code true} if the tabs changed
     259     */
     260    private void updateLayers(boolean changed) {
     261        MainLayerManager layerManager = MainApplication.getLayerManager();
     262        List<IGeoImageLayer> geoImageLayers = layerManager.getLayers().stream()
     263                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     264        if (geoImageLayers.isEmpty()) {
    236265            this.layers.setVisible(false);
    237             this.layers.removeAll();
    238266        } else {
    239267            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();
     268            if (changed) {
     269                addButtonsForImageLayers();
     270            }
     271            MoveImgDisplayPanel<?> selected = (MoveImgDisplayPanel<?>) this.layers.getSelectedComponent();
     272            if ((this.imgDisplay.getParent() == null || this.imgDisplay.getParent().getParent() == null)
     273                && selected != null && selected.layer.containsImage(this.currentEntry)) {
     274                selected.setVisible(selected.isVisible());
     275            } else if (selected != null && !selected.layer.containsImage(this.currentEntry)) {
     276                this.getImageTabs().filter(m -> m.layer.containsImage(this.currentEntry)).mapToInt(this.layers::indexOfComponent).findFirst()
     277                        .ifPresent(this.layers::setSelectedIndex);
     278            }
    250279            this.layers.invalidate();
    251280        }
     281        this.layers.getParent().invalidate();
    252282        this.revalidate();
    253283    }
    254284
     
    256286     * Add the buttons for image layers
    257287     */
    258288    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));
    269         }
    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));
     289        List<MoveImgDisplayPanel<?>> alreadyAdded = this.getImageTabs().collect(Collectors.toList());
     290        // Avoid the setVisible call recursively calling this method and adding duplicates
     291        alreadyAdded.forEach(m -> m.finishedAddingButtons = false);
     292        List<Layer> availableLayers = MainApplication.getLayerManager().getLayers();
     293        List<IGeoImageLayer> geoImageLayers = availableLayers.stream()
     294                .sorted(Comparator.comparingInt(entry -> /*reverse*/-availableLayers.indexOf(entry)))
     295                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     296        for (IGeoImageLayer layer : geoImageLayers) {
     297            final Optional<MoveImgDisplayPanel<?>> originalPanel = alreadyAdded.stream()
     298                    .filter(m -> Objects.equals(m.layer, layer)).findFirst();
     299            if (originalPanel.isPresent()) {
     300                MoveImgDisplayPanel<?> panel = originalPanel.get();
     301                int componentIndex = this.layers.indexOfComponent(panel);
     302                if (componentIndex == geoImageLayers.indexOf(layer)) {
     303                    this.layers.setTitleAt(componentIndex, panel.getLabel(availableLayers));
     304                } else {
     305                    boolean isSelected = this.layers.getSelectedComponent() == panel;
     306                    this.removeImageTab((Layer) layer);
     307                    this.layers.insertTab(panel.getLabel(availableLayers), null, panel, null, geoImageLayers.indexOf(layer));
     308                    if (isSelected) {
     309                        this.layers.setSelectedComponent(panel);
     310                    }
     311                }
     312            } else {
     313                MoveImgDisplayPanel<?> panel = new MoveImgDisplayPanel<>(this.imgDisplay, (Layer & IGeoImageLayer) layer);
     314                this.layers.addTab(panel.getLabel(availableLayers), panel);
     315            }
    276316        }
    277         layerButtons.forEach(this.layers::add);
     317        this.getImageTabs().map(p -> p.layer).filter(layer -> !availableLayers.contains(layer))
     318                // We have to collect to a list prior to removal -- if we don't, then the stream may get a layer at index 0,
     319                // remove that layer, and then get a layer at index 1, which was previously at index 2.
     320                .collect(Collectors.toList()).forEach(this::removeImageTab);
     321
     322        // This is need to avoid the first button becoming visible, and then recalling this method.
     323        this.getImageTabs().forEach(m -> m.finishedAddingButtons = true);
     324        // After that, trigger the visibility set code
     325        this.getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
    278326    }
    279327
    280328    /**
    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
     329     * Remove a tab for a layer from the {@link #layers} tab pane
     330     * @param layer The layer to remove
    286331     */
    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;
     332    private void removeImageTab(Layer layer) {
     333        // This must be reversed to avoid removing the wrong tab
     334        for (int i = this.layers.getTabCount() - 1; i >= 0; i--) {
     335            Component component = this.layers.getComponentAt(i);
     336            if (component instanceof MoveImgDisplayPanel) {
     337                MoveImgDisplayPanel<?> moveImgDisplayPanel = (MoveImgDisplayPanel<?>) component;
     338                if (Objects.equals(layer, moveImgDisplayPanel.layer)) {
     339                    this.layers.removeTabAt(i);
     340                    this.layers.remove(moveImgDisplayPanel);
     341                }
     342            }
     343        }
     344    }
     345
     346    /**
     347     * Get the {@link MoveImgDisplayPanel} objects in {@link #layers}.
     348     * @return The individual panels
     349     */
     350    private Stream<MoveImgDisplayPanel<?>> getImageTabs() {
     351        return IntStream.range(0, this.layers.getTabCount())
     352                .mapToObj(this.layers::getComponentAt)
     353                .filter(MoveImgDisplayPanel.class::isInstance)
     354                .map(m -> (MoveImgDisplayPanel<?>) m);
    292355    }
    293356
    294357    @Override
     
    309372        imageZoomAction.destroy();
    310373        cancelLoadingImage();
    311374        super.destroy();
    312         dialog = null;
     375        destroyInstance();
    313376    }
    314377
    315378    /**
     
    433496        }
    434497    }
    435498
    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 
    455499    private class ImageFirstAction extends ImageRememberAction {
    456500        ImageFirstAction() {
    457501            super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
     
    478522        public void actionPerformed(ActionEvent e) {
    479523            final JToggleButton button = (JToggleButton) e.getSource();
    480524            centerView = button.isEnabled() && button.isSelected();
     525            Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView);
    481526            if (centerView && currentEntry != null && currentEntry.getPos() != null) {
    482527                MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
    483528            }
     
    618663        }
    619664    }
    620665
     666    /**
     667     * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay arround and (b) setting the imgDisplay as a child
     668     * for this panel.
     669     */
     670    private static class MoveImgDisplayPanel<T extends Layer & IGeoImageLayer> extends JPanel {
     671        private final T layer;
     672        private final ImageDisplay imgDisplay;
     673
     674        /**
     675         * 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
     676         * has multiple tabs on initialization (like from a session).
     677         */
     678        boolean finishedAddingButtons;
     679        MoveImgDisplayPanel(ImageDisplay imgDisplay, T layer) {
     680            super(new BorderLayout());
     681            this.layer = layer;
     682            this.imgDisplay = imgDisplay;
     683        }
     684
     685        @Override
     686        public void setVisible(boolean visible) {
     687            super.setVisible(visible);
     688            JTabbedPane layers = ImageViewerDialog.getInstance().layers;
     689            int index = layers.indexOfComponent(this);
     690            if (visible && this.finishedAddingButtons) {
     691                if (!this.layer.getSelection().isEmpty() && !this.layer.getSelection().contains(ImageViewerDialog.getCurrentImage())) {
     692                    ImageViewerDialog.getInstance().displayImages(this.layer.getSelection());
     693                    this.layer.invalidate(); // This will force the geoimage layers to update properly.
     694                }
     695                if (this.imgDisplay.getParent() != this) {
     696                    this.add(this.imgDisplay, BorderLayout.CENTER);
     697                    this.imgDisplay.invalidate();
     698                    this.revalidate();
     699                }
     700                if (index >= 0) {
     701                    layers.setTitleAt(index, "* " + getLabel(MainApplication.getLayerManager().getLayers()));
     702                }
     703            } else if (index >= 0) {
     704                layers.setTitleAt(index, getLabel(MainApplication.getLayerManager().getLayers()));
     705            }
     706        }
     707
     708        /**
     709         * Get the label for this panel
     710         * @param availableLayers The layers to use to get the index
     711         * @return The label for this layer
     712         */
     713        String getLabel(List<Layer> availableLayers) {
     714            final int index = availableLayers.size() - availableLayers.indexOf(layer);
     715            return (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + layer.getLabel();
     716        }
     717    }
     718
    621719    /**
    622720     * Enables (or disables) the "Previous" button.
    623721     * @param value {@code true} to enable the button, {@code false} otherwise
     
    651749        return wasEnabled;
    652750    }
    653751
    654     /** Used for tabbed panes */
    655     private final transient Map<Layer, List<IImageEntry<?>>> tabbedEntries = new HashMap<>();
    656752    private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
    657753
    658754    /**
     
    679775     * @param entries image entries
    680776     * @since 18246
    681777     */
    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) {
     778    public void displayImages(List<? extends IImageEntry<?>> entries) {
    693779        boolean imageChanged;
    694780        IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
    695781
     
    710796            }
    711797        }
    712798
    713         if (entries == null || entries.isEmpty() || entries.stream().allMatch(Objects::isNull)) {
    714             this.tabbedEntries.remove(layer);
     799
     800        final boolean updateRequired;
     801        final List<IGeoImageLayer> imageLayers = MainApplication.getLayerManager().getLayers().stream()
     802                    .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     803        if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) {
     804            updateRequired = true;
     805            // Clear the selected images in other geoimage layers
     806            this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast)
     807                    .filter(l -> !Objects.equals(entries, l.getSelection()))
     808                    .forEach(IGeoImageLayer::clearSelection);
    715809        } else {
    716             this.tabbedEntries.put(layer, entries);
     810            updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals));
    717811        }
    718         this.updateLayers();
     812        this.updateLayers(updateRequired);
    719813        if (entry != null) {
    720814            this.updateButtonsNonNullEntry(entry, imageChanged);
    721         } else if (this.tabbedEntries.isEmpty()) {
     815        } else if (imageLayers.isEmpty()) {
    722816            this.updateButtonsNullEntry(entries);
    723817            return;
    724818        } 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) {
     819            IGeoImageLayer layer = imageLayers.stream().filter(l -> l.getSelection().size() == 1).findFirst().orElse(null);
     820            if (layer == null) {
    728821                this.updateButtonsNullEntry(entries);
    729822            } else {
    730                 this.displayImages(realEntry.getKey(), realEntry.getValue());
     823                this.displayImages(layer.getSelection());
    731824            }
    732825            return;
    733826        }
     
    744837     * Update buttons for null entry
    745838     * @param entries {@code true} if multiple images are selected
    746839     */
    747     private void updateButtonsNullEntry(List<IImageEntry<?>> entries) {
     840    private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) {
    748841        boolean hasMultipleImages = entries != null && entries.size() > 1;
    749842        // if this method is called to reinitialize dialog content with a blank image,
    750843        // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
    751         setTitle(tr("Geotagged Images"));
     844        this.updateTitle();
    752845        imgDisplay.setImage(null);
    753846        imgDisplay.setOsdText("");
    754847        setNextEnabled(false);
     
    787880        btnCopyPath.setEnabled(true);
    788881        btnOpenExternal.setEnabled(true);
    789882
    790         setTitle(tr("Geotagged Images") + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
     883        this.updateTitle();
    791884        StringBuilder osd = new StringBuilder(entry.getDisplayName());
    792885        if (entry.getElevation() != null) {
    793886            osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
     
    818911        imgDisplay.setOsdText(osd.toString());
    819912    }
    820913
    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);
     914    private void updateTitle() {
     915        final IImageEntry<?> entry;
     916        synchronized (this) {
     917            entry = this.currentEntry;
     918        }
     919        String baseTitle = Optional.of(this.layers.getSelectedComponent())
     920                .filter(MoveImgDisplayPanel.class::isInstance).map(MoveImgDisplayPanel.class::cast)
     921                .map(m -> m.layer).map(Layer::getLabel).orElse(tr("Geotagged Images"));
     922        if (entry == null) {
     923            this.setTitle(baseTitle);
     924        } else {
     925            this.setTitle(baseTitle + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
     926        }
    831927    }
    832928
    833     private static boolean isLastImageSelected(List<IImageEntry<?>> data) {
     929    private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) {
    834930        return data.stream().anyMatch(image -> data.contains(image.getLastImage()));
    835931    }
    836932
    837     private static boolean isFirstImageSelected(List<IImageEntry<?>> data) {
     933    private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) {
    838934        return data.stream().anyMatch(image -> data.contains(image.getFirstImage()));
    839935    }
    840936
     
    857953        if (btnCollapse != null) {
    858954            btnCollapse.setVisible(!isDocked);
    859955        }
    860         this.updateLayers();
     956        this.updateLayers(true);
    861957    }
    862958
    863959    /**
     
    9041000
    9051001    @Override
    9061002    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);
     1003        if (e.getRemovedLayer() instanceof IGeoImageLayer && ((IGeoImageLayer) e.getRemovedLayer()).containsImage(this.currentEntry)) {
     1004            displayImages(null);
    9131005        }
    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());
     1006        this.updateLayers(true);
    9161007    }
    9171008
    9181009    @Override
    9191010    public void layerOrderChanged(LayerOrderChangeEvent e) {
    920         // ignored
     1011        this.updateLayers(true);
    9211012    }
    9221013
    9231014    @Override
     
    9411032    }
    9421033
    9431034    private void registerOnLayer(Layer layer) {
    944         if (layer instanceof GeoImageLayer) {
    945             ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
     1035        if (layer instanceof IGeoImageLayer) {
     1036            layer.addPropertyChangeListener(l -> {
     1037                final List<?> currentTabLayers = this.getImageTabs().map(m -> m.layer).collect(Collectors.toList());
     1038                if (Layer.NAME_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
     1039                    this.updateLayers(true);
     1040                        if (((IGeoImageLayer) layer).containsImage(this.currentEntry)) {
     1041                            this.updateTitle();
     1042                        }
     1043                } // Use Layer.VISIBLE_PROP here if we decide to do something when layer visibility changes
     1044            });
    9461045        }
    9471046    }
    9481047
     
    9591058            imgLoadingFuture = null;
    9601059        }
    9611060    }
    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     }
    9751061}
  • 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}