Ticket #21605: 21605.7.patch

File 21605.7.patch, 56.1 KB (added by taylor.smock, 2 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;
     
    6265import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker;
    6366import org.openstreetmap.josm.gui.layer.Layer;
    6467import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
     68import org.openstreetmap.josm.gui.util.GuiHelper;
    6569import org.openstreetmap.josm.gui.util.imagery.Vector3D;
    6670import org.openstreetmap.josm.tools.ImageProvider;
     71import org.openstreetmap.josm.tools.ListenerList;
    6772import org.openstreetmap.josm.tools.Utils;
    6873
    6974/**
     
    7176 * @since 99
    7277 */
    7378public class GeoImageLayer extends AbstractModifiableLayer implements
    74         JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener {
     79        JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener,
     80        IGeoImageLayer {
    7581
    7682    private static final List<Action> menuAdditions = new LinkedList<>();
    7783
    7884    private static volatile List<MapMode> supportedMapModes;
    7985
    8086    private final ImageData data;
     87    private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListeners = ListenerList.create();
    8188    GpxData gpxData;
    8289    GpxLayer gpxFauxLayer;
    8390    GpxData gpxFauxData;
     
    8693
    8794    private final Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker");
    8895    private final Icon selectedIcon = ImageProvider.get("dialogs/geoimage/photo-marker-selected");
     96    private final Icon selectedIconNotImageViewer = generateSelectedIconNotImageViewer(this.selectedIcon);
     97
     98    private static Icon generateSelectedIconNotImageViewer(Icon selectedIcon) {
     99        Color color = new NamedColorProperty("geoimage.selected.not.image.viewer", new Color(50, 0, 0)).get();
     100        BufferedImage bi = new BufferedImage(selectedIcon.getIconWidth(), selectedIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
     101        Graphics2D g2d = bi.createGraphics();
     102        selectedIcon.paintIcon(null, g2d, 0, 0);
     103        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f));
     104        g2d.setColor(color);
     105        g2d.fillRect(0, 0, selectedIcon.getIconWidth(), selectedIcon.getIconHeight());
     106        g2d.dispose();
     107        return new ImageIcon(bi);
     108    }
    89109
    90110    boolean useThumbs;
    91111    private final ExecutorService thumbsLoaderExecutor =
     
    172192        this.useThumbs = useThumbs;
    173193        this.data.addImageDataUpdateListener(this);
    174194        this.data.setLayer(this);
     195        synchronized (ImageViewerDialog.class) {
     196            if (!ImageViewerDialog.hasInstance()) {
     197                GuiHelper.runInEDTAndWait(ImageViewerDialog::createInstance);
     198            }
     199        }
     200        if (getInvalidGeoImages().size() == data.size()) {
     201            ImageViewerDialog.getInstance().displayImages(Collections.singletonList(this.data.getFirstImage()));
     202        }
    175203    }
    176204
    177205    private final class ImageMouseListener extends MouseAdapter {
     
    232260                    }
    233261                } else {
    234262                    data.setSelectedImage(img);
    235                     ImageViewerDialog.getInstance().displayImages(GeoImageLayer.this, Collections.singletonList(img));
     263                    ImageViewerDialog.getInstance().displayImages(Collections.singletonList(img));
    236264                }
     265                GeoImageLayer.this.invalidate(); // Needed to update which image is being shown in the image viewer in the mapview
    237266            }
    238267        }
    239268    }
     
    247276        MainApplication.worker.execute(new ImagesLoader(files, gpxLayer));
    248277    }
    249278
     279    @Override
     280    public void clearSelection() {
     281        this.getImageData().clearSelectedImage();
     282    }
     283
     284    @Override
     285    public boolean containsImage(IImageEntry<?> imageEntry) {
     286        if (imageEntry instanceof ImageEntry) {
     287            return this.data.getImages().contains(imageEntry);
     288        }
     289        return false;
     290    }
     291
    250292    @Override
    251293    public Icon getIcon() {
    252294        return ImageProvider.get("dialogs/geoimage", ImageProvider.ImageSizes.LAYER);
    253295    }
    254296
     297    @Override
     298    public List<ImageEntry> getSelection() {
     299        return this.getImageData().getSelectedImages();
     300    }
     301
     302    @Override
     303    public List<IImageEntry<?>> getInvalidGeoImages() {
     304        return this.getImageData().getImages().stream().filter(entry -> entry.getPos() == null || entry.getExifCoor() == null
     305              || !entry.getExifCoor().isValid() || !entry.getPos().isValid()).collect(toList());
     306    }
     307
     308    @Override
     309    public void addImageChangeListener(ImageChangeListener listener) {
     310        this.imageChangeListeners.addListener(listener);
     311    }
     312
     313    @Override
     314    public void removeImageChangeListener(ImageChangeListener listener) {
     315        this.imageChangeListeners.removeListener(listener);
     316    }
     317
    255318    /**
    256319     * Register actions on the layer
    257320     * @param addition the action to be added
     
    451514            }
    452515        }
    453516
     517        final IImageEntry<?> currentImage = ImageViewerDialog.getCurrentImage();
    454518        for (ImageEntry e: data.getSelectedImages()) {
    455519            if (e != null && e.getPos() != null) {
    456520                Point p = mv.getPoint(e.getPos());
     
    465529                if (useThumbs && e.hasThumbnail()) {
    466530                    g.setColor(new Color(128, 0, 0, 122));
    467531                    g.fillRect(p.x - imgDim.width / 2, p.y - imgDim.height / 2, imgDim.width, imgDim.height);
     532                } else if (e.equals(currentImage)) {
     533                    selectedIcon.paintIcon(mv, g,
     534                            p.x - imgDim.width / 2,
     535                            p.y - imgDim.height / 2);
    468536                } else {
    469                     selectedIcon.paintIcon(mv, g,
     537                    selectedIconNotImageViewer.paintIcon(mv, g,
    470538                            p.x - imgDim.width / 2,
    471539                            p.y - imgDim.height / 2);
    472540                }
     
    884952    @Override
    885953    public void selectedImageChanged(ImageData data) {
    886954        showCurrentPhoto();
     955        this.imageChangeListeners.fireEvent(e -> e.imageChanged(this, null, data.getSelectedImages()));
    887956    }
    888957
    889958    @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  
    1414import java.awt.event.ActionListener;
    1515import java.awt.event.KeyEvent;
    1616import java.awt.event.WindowEvent;
     17import java.beans.PropertyChangeEvent;
     18import java.beans.PropertyChangeListener;
    1719import java.io.IOException;
    1820import java.io.Serializable;
    1921import java.time.ZoneOffset;
     
    2325import java.util.Arrays;
    2426import java.util.Collections;
    2527import java.util.Comparator;
    26 import java.util.HashMap;
    2728import java.util.List;
    28 import java.util.Map;
    2929import java.util.Objects;
    3030import java.util.Optional;
    3131import java.util.concurrent.Future;
    3232import java.util.function.UnaryOperator;
    3333import java.util.stream.Collectors;
     34import java.util.stream.IntStream;
     35import java.util.stream.Stream;
    3436
    3537import javax.swing.AbstractAction;
     38import javax.swing.AbstractButton;
     39import javax.swing.BorderFactory;
    3640import javax.swing.Box;
    3741import javax.swing.JButton;
    3842import javax.swing.JLabel;
    3943import javax.swing.JOptionPane;
    4044import javax.swing.JPanel;
     45import javax.swing.JTabbedPane;
    4146import javax.swing.JToggleButton;
    4247import javax.swing.SwingConstants;
    4348import javax.swing.SwingUtilities;
    4449
     50import org.openstreetmap.josm.actions.ExpertToggleAction;
    4551import org.openstreetmap.josm.actions.JosmAction;
    4652import org.openstreetmap.josm.data.ImageData;
    47 import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;
    4853import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
    4954import org.openstreetmap.josm.gui.ExtendedDialog;
    5055import org.openstreetmap.josm.gui.MainApplication;
     
    6469import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
    6570import org.openstreetmap.josm.gui.util.GuiHelper;
    6671import org.openstreetmap.josm.gui.util.imagery.Vector3D;
     72import org.openstreetmap.josm.gui.widgets.HideableTabbedPane;
     73import org.openstreetmap.josm.spi.preferences.Config;
    6774import org.openstreetmap.josm.tools.ImageProvider;
    6875import org.openstreetmap.josm.tools.Logging;
    6976import org.openstreetmap.josm.tools.PlatformManager;
     
    7380/**
    7481 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
    7582 */
    76 public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener, ImageDataUpdateListener {
     83public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
    7784    private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}");
    7885    private static final String DIALOG_FOLDER = "dialogs";
    7986
     
    124131        return dialog;
    125132    }
    126133
     134    /**
     135     * Check if there is an instance for the {@link ImageViewerDialog}
     136     * @return {@code true} if there is a static singleton instance of {@link ImageViewerDialog}
     137     * @since xxx
     138     */
     139    public static boolean hasInstance() {
     140        return dialog != null;
     141    }
     142
     143    /**
     144     * Destroy the current dialog
     145     */
     146    private static void destroyInstance() {
     147        dialog = null;
     148    }
     149
    127150    private JButton btnLast;
    128151    private JButton btnNext;
    129152    private JButton btnPrevious;
     
    135158    private JButton btnDeleteFromDisk;
    136159    private JToggleButton tbCentre;
    137160    /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */
    138     private JPanel layers;
     161    private final HideableTabbedPane layers = new HideableTabbedPane();
    139162
    140163    private ImageViewerDialog() {
    141164        super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
     
    168191
    169192    private void build() {
    170193        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);
     194        content.add(this.layers, BorderLayout.CENTER);
    175195
    176196        Dimension buttonDim = new Dimension(26, 26);
    177197
     
    187207        btnLast = createNavigationButton(imageLastAction, buttonDim);
    188208
    189209        tbCentre = new JToggleButton(imageCenterViewAction);
     210        tbCentre.setSelected(Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false));
    190211        tbCentre.setPreferredSize(buttonDim);
    191212
    192213        JButton btnZoomBestFit = new JButton(imageZoomAction);
     
    196217        btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
    197218
    198219        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));
     220        addButtonGroup(buttons, this.btnFirst, this.btnPrevious, this.btnNext, this.btnLast);
     221        addButtonGroup(buttons, this.tbCentre, btnZoomBestFit);
     222        addButtonGroup(buttons, this.btnDelete, this.btnDeleteFromDisk);
     223        addButtonGroup(buttons, this.btnCopyPath, this.btnOpenExternal);
     224        addButtonGroup(buttons, createButton(visibilityAction, buttonDim));
    214225
    215226        JPanel bottomPane = new JPanel(new GridBagLayout());
    216227        GridBagConstraints gc = new GridBagConstraints();
     
    231242        createLayout(content, false, null);
    232243    }
    233244
    234     private void updateLayers() {
    235         if (this.tabbedEntries.size() <= 1) {
     245    /**
     246     * Add a button group to a panel
     247     * @param buttonPanel The panel holding the buttons
     248     * @param buttons The button group to add
     249     */
     250    private static void addButtonGroup(JPanel buttonPanel, AbstractButton... buttons) {
     251        if (buttonPanel.getComponentCount() != 0) {
     252            buttonPanel.add(Box.createRigidArea(new Dimension(7, 0)));
     253        }
     254
     255        for (AbstractButton jButton : buttons) {
     256            buttonPanel.add(jButton);
     257        }
     258    }
     259
     260    /**
     261     * Update the tabs for the different image layers
     262     * @param changed {@code true} if the tabs changed
     263     */
     264    private void updateLayers(boolean changed) {
     265        MainLayerManager layerManager = MainApplication.getLayerManager();
     266        List<IGeoImageLayer> geoImageLayers = layerManager.getLayers().stream()
     267                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     268        if (geoImageLayers.isEmpty()) {
    236269            this.layers.setVisible(false);
    237             this.layers.removeAll();
    238270        } else {
    239271            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();
     272            if (changed) {
     273                addButtonsForImageLayers();
     274            }
     275            MoveImgDisplayPanel<?> selected = (MoveImgDisplayPanel<?>) this.layers.getSelectedComponent();
     276            if ((this.imgDisplay.getParent() == null || this.imgDisplay.getParent().getParent() == null)
     277                && selected != null && selected.layer.containsImage(this.currentEntry)) {
     278                selected.setVisible(selected.isVisible());
     279            } else if (selected != null && !selected.layer.containsImage(this.currentEntry)) {
     280                this.getImageTabs().filter(m -> m.layer.containsImage(this.currentEntry)).mapToInt(this.layers::indexOfComponent).findFirst()
     281                        .ifPresent(this.layers::setSelectedIndex);
     282            }
    250283            this.layers.invalidate();
    251284        }
     285        this.layers.getParent().invalidate();
    252286        this.revalidate();
    253287    }
    254288
     
    256290     * Add the buttons for image layers
    257291     */
    258292    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));
     293        List<MoveImgDisplayPanel<?>> alreadyAdded = this.getImageTabs().collect(Collectors.toList());
     294        // Avoid the setVisible call recursively calling this method and adding duplicates
     295        alreadyAdded.forEach(m -> m.finishedAddingButtons = false);
     296        List<Layer> availableLayers = MainApplication.getLayerManager().getLayers();
     297        List<IGeoImageLayer> geoImageLayers = availableLayers.stream()
     298                .sorted(Comparator.comparingInt(entry -> /*reverse*/-availableLayers.indexOf(entry)))
     299                .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     300        List<IGeoImageLayer> tabLayers = geoImageLayers.stream()
     301                .filter(l -> alreadyAdded.stream().anyMatch(m -> Objects.equals(l, m.layer)) || l.containsImage(this.currentEntry))
     302                .collect(Collectors.toList());
     303        for (IGeoImageLayer layer : tabLayers) {
     304            final MoveImgDisplayPanel<?> panel = alreadyAdded.stream()
     305                    .filter(m -> Objects.equals(m.layer, layer)).findFirst()
     306                    .orElseGet(() -> new MoveImgDisplayPanel<>(this.imgDisplay, (Layer & IGeoImageLayer) layer));
     307            int componentIndex = this.layers.indexOfComponent(panel);
     308            if (componentIndex == geoImageLayers.indexOf(layer)) {
     309                this.layers.setTitleAt(componentIndex, panel.getLabel(availableLayers));
     310            } else {
     311                this.removeImageTab((Layer) layer);
     312                this.layers.insertTab(panel.getLabel(availableLayers), null, panel, null, tabLayers.indexOf(layer));
     313                int idx = this.layers.indexOfComponent(panel);
     314                CloseableTab closeableTab = new CloseableTab(l -> {
     315                    Component source = (Component) l.getSource();
     316                    int index = layers.indexOfTabComponent(source);
     317                    while (index < 0 && source != null) {
     318                        index = layers.indexOfTabComponent(source);
     319                        source = source.getParent();
     320                        if (index >= 0) {
     321                            getImageTabs().forEach(m -> m.finishedAddingButtons = false);
     322                            removeImageTab(((MoveImgDisplayPanel<?>) layers.getComponentAt(index)).layer);
     323                            getImageTabs().forEach(m -> m.finishedAddingButtons = true);
     324                            getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
     325                            layers.revalidate();
     326                            return;
     327                        }
     328                    }
     329                });
     330                this.layers.addPropertyChangeListener("indexForTitle", closeableTab);
     331                this.layers.setTabComponentAt(idx, closeableTab);
     332            }
     333            if (layer.containsImage(this.currentEntry)) {
     334                this.layers.setSelectedComponent(panel);
     335            }
    269336        }
    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));
     337        this.getImageTabs().map(p -> p.layer).filter(layer -> !availableLayers.contains(layer))
     338                // We have to collect to a list prior to removal -- if we don't, then the stream may get a layer at index 0,
     339                // remove that layer, and then get a layer at index 1, which was previously at index 2.
     340                .collect(Collectors.toList()).forEach(this::removeImageTab);
     341
     342        // This is need to avoid the first button becoming visible, and then recalling this method.
     343        this.getImageTabs().forEach(m -> m.finishedAddingButtons = true);
     344        // After that, trigger the visibility set code
     345        this.getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
     346    }
     347
     348    /**
     349     * Remove a tab for a layer from the {@link #layers} tab pane
     350     * @param layer The layer to remove
     351     */
     352    private void removeImageTab(Layer layer) {
     353        // This must be reversed to avoid removing the wrong tab
     354        for (int i = this.layers.getTabCount() - 1; i >= 0; i--) {
     355            Component component = this.layers.getComponentAt(i);
     356            if (component instanceof MoveImgDisplayPanel) {
     357                MoveImgDisplayPanel<?> moveImgDisplayPanel = (MoveImgDisplayPanel<?>) component;
     358                if (Objects.equals(layer, moveImgDisplayPanel.layer)) {
     359                    this.layers.removeTabAt(i);
     360                    this.layers.remove(moveImgDisplayPanel);
     361                }
     362            }
    276363        }
    277         layerButtons.forEach(this.layers::add);
    278364    }
    279365
    280366    /**
    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
     367     * Get the {@link MoveImgDisplayPanel} objects in {@link #layers}.
     368     * @return The individual panels
    286369     */
    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;
     370    private Stream<MoveImgDisplayPanel<?>> getImageTabs() {
     371        return IntStream.range(0, this.layers.getTabCount())
     372                .mapToObj(this.layers::getComponentAt)
     373                .filter(MoveImgDisplayPanel.class::isInstance)
     374                .map(m -> (MoveImgDisplayPanel<?>) m);
    292375    }
    293376
    294377    @Override
     
    309392        imageZoomAction.destroy();
    310393        cancelLoadingImage();
    311394        super.destroy();
    312         dialog = null;
     395        destroyInstance();
    313396    }
    314397
    315398    /**
     
    433516        }
    434517    }
    435518
    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 
    455519    private class ImageFirstAction extends ImageRememberAction {
    456520        ImageFirstAction() {
    457521            super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
     
    478542        public void actionPerformed(ActionEvent e) {
    479543            final JToggleButton button = (JToggleButton) e.getSource();
    480544            centerView = button.isEnabled() && button.isSelected();
     545            Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView);
    481546            if (centerView && currentEntry != null && currentEntry.getPos() != null) {
    482547                MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
    483548            }
     
    618683        }
    619684    }
    620685
     686    /**
     687     * A tab title renderer for {@link HideableTabbedPane} that allows us to close tabs.
     688     * It should be added to the listeners for {@code "indexForTitle"} property changes.
     689     * See {@link HideableTabbedPane#addPropertyChangeListener(String, PropertyChangeListener)}.
     690     */
     691    private static class CloseableTab extends JPanel implements PropertyChangeListener {
     692        private final JLabel title;
     693
     694        /**
     695         * Create a new {@link CloseableTab}.
     696         * @param closeAction The action to run to close the tab. You probably want to call {@link JTabbedPane#removeTabAt(int)}
     697         *                    at the very least.
     698         */
     699        CloseableTab(ActionListener closeAction) {
     700            this.title = new JLabel();
     701            this.add(this.title);
     702            JButton close = new JButton(ImageProvider.get("misc", "close"));
     703            close.setBorder(BorderFactory.createEmptyBorder());
     704            this.add(close);
     705            close.addActionListener(closeAction);
     706        }
     707
     708        @Override
     709        public void propertyChange(PropertyChangeEvent evt) {
     710            if ("indexForTitle".equals(evt.getPropertyName()) && evt.getSource() instanceof JTabbedPane) {
     711                JTabbedPane source = (JTabbedPane) evt.getSource();
     712                int idx = source.indexOfTabComponent(this);
     713                if (idx >= 0) {
     714                    this.title.setText(source.getTitleAt(idx));
     715                }
     716            }
     717        }
     718    }
     719
     720    /**
     721     * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay arround and (b) setting the imgDisplay as a child
     722     * for this panel.
     723     */
     724    private static class MoveImgDisplayPanel<T extends Layer & IGeoImageLayer> extends JPanel {
     725        private final T layer;
     726        private final ImageDisplay imgDisplay;
     727
     728        /**
     729         * 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
     730         * has multiple tabs on initialization (like from a session).
     731         */
     732        boolean finishedAddingButtons;
     733        MoveImgDisplayPanel(ImageDisplay imgDisplay, T layer) {
     734            super(new BorderLayout());
     735            this.layer = layer;
     736            this.imgDisplay = imgDisplay;
     737        }
     738
     739        @Override
     740        public void setVisible(boolean visible) {
     741            super.setVisible(visible);
     742            JTabbedPane layers = ImageViewerDialog.getInstance().layers;
     743            int index = layers.indexOfComponent(this);
     744            if (visible && this.finishedAddingButtons) {
     745                if (!this.layer.getSelection().isEmpty() && !this.layer.getSelection().contains(ImageViewerDialog.getCurrentImage())) {
     746                    ImageViewerDialog.getInstance().displayImages(this.layer.getSelection());
     747                    this.layer.invalidate(); // This will force the geoimage layers to update properly.
     748                }
     749                if (this.imgDisplay.getParent() != this) {
     750                    this.add(this.imgDisplay, BorderLayout.CENTER);
     751                    this.imgDisplay.invalidate();
     752                    this.revalidate();
     753                }
     754                if (index >= 0) {
     755                    layers.setTitleAt(index, "* " + getLabel(MainApplication.getLayerManager().getLayers()));
     756                }
     757            } else if (index >= 0) {
     758                layers.setTitleAt(index, getLabel(MainApplication.getLayerManager().getLayers()));
     759            }
     760        }
     761
     762        /**
     763         * Get the label for this panel
     764         * @param availableLayers The layers to use to get the index
     765         * @return The label for this layer
     766         */
     767        String getLabel(List<Layer> availableLayers) {
     768            final int index = availableLayers.size() - availableLayers.indexOf(layer);
     769            return (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + layer.getLabel();
     770        }
     771    }
     772
    621773    /**
    622774     * Enables (or disables) the "Previous" button.
    623775     * @param value {@code true} to enable the button, {@code false} otherwise
     
    651803        return wasEnabled;
    652804    }
    653805
    654     /** Used for tabbed panes */
    655     private final transient Map<Layer, List<IImageEntry<?>>> tabbedEntries = new HashMap<>();
    656806    private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
    657807
    658808    /**
     
    679829     * @param entries image entries
    680830     * @since 18246
    681831     */
    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) {
     832    public void displayImages(List<? extends IImageEntry<?>> entries) {
    693833        boolean imageChanged;
    694834        IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
    695835
     
    710850            }
    711851        }
    712852
    713         if (entries == null || entries.isEmpty() || entries.stream().allMatch(Objects::isNull)) {
    714             this.tabbedEntries.remove(layer);
     853
     854        final boolean updateRequired;
     855        final List<IGeoImageLayer> imageLayers = MainApplication.getLayerManager().getLayers().stream()
     856                    .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
     857        if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) {
     858            updateRequired = true;
     859            // Clear the selected images in other geoimage layers
     860            this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast)
     861                    .filter(l -> !Objects.equals(entries, l.getSelection()))
     862                    .forEach(IGeoImageLayer::clearSelection);
    715863        } else {
    716             this.tabbedEntries.put(layer, entries);
     864            updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals));
    717865        }
    718         this.updateLayers();
     866        this.updateLayers(updateRequired);
    719867        if (entry != null) {
    720868            this.updateButtonsNonNullEntry(entry, imageChanged);
    721         } else if (this.tabbedEntries.isEmpty()) {
     869        } else if (imageLayers.isEmpty()) {
    722870            this.updateButtonsNullEntry(entries);
    723871            return;
    724872        } 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) {
     873            IGeoImageLayer layer = this.getImageTabs().map(m -> m.layer).filter(l -> l.getSelection().size() == 1).findFirst().orElse(null);
     874            if (layer == null) {
    728875                this.updateButtonsNullEntry(entries);
    729876            } else {
    730                 this.displayImages(realEntry.getKey(), realEntry.getValue());
     877                this.displayImages(layer.getSelection());
    731878            }
    732879            return;
    733880        }
     
    744891     * Update buttons for null entry
    745892     * @param entries {@code true} if multiple images are selected
    746893     */
    747     private void updateButtonsNullEntry(List<IImageEntry<?>> entries) {
     894    private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) {
    748895        boolean hasMultipleImages = entries != null && entries.size() > 1;
    749896        // if this method is called to reinitialize dialog content with a blank image,
    750897        // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
    751         setTitle(tr("Geotagged Images"));
     898        this.updateTitle();
    752899        imgDisplay.setImage(null);
    753900        imgDisplay.setOsdText("");
    754901        setNextEnabled(false);
     
    787934        btnCopyPath.setEnabled(true);
    788935        btnOpenExternal.setEnabled(true);
    789936
    790         setTitle(tr("Geotagged Images") + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
     937        this.updateTitle();
    791938        StringBuilder osd = new StringBuilder(entry.getDisplayName());
    792939        if (entry.getElevation() != null) {
    793940            osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
     
    818965        imgDisplay.setOsdText(osd.toString());
    819966    }
    820967
    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);
     968    private void updateTitle() {
     969        final IImageEntry<?> entry;
     970        synchronized (this) {
     971            entry = this.currentEntry;
     972        }
     973        String baseTitle = Optional.ofNullable(this.layers.getSelectedComponent())
     974                .filter(MoveImgDisplayPanel.class::isInstance).map(MoveImgDisplayPanel.class::cast)
     975                .map(m -> m.layer).map(Layer::getLabel).orElse(tr("Geotagged Images"));
     976        if (entry == null) {
     977            this.setTitle(baseTitle);
     978        } else {
     979            this.setTitle(baseTitle + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
     980        }
    831981    }
    832982
    833     private static boolean isLastImageSelected(List<IImageEntry<?>> data) {
     983    private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) {
    834984        return data.stream().anyMatch(image -> data.contains(image.getLastImage()));
    835985    }
    836986
    837     private static boolean isFirstImageSelected(List<IImageEntry<?>> data) {
     987    private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) {
    838988        return data.stream().anyMatch(image -> data.contains(image.getFirstImage()));
    839989    }
    840990
     
    8571007        if (btnCollapse != null) {
    8581008            btnCollapse.setVisible(!isDocked);
    8591009        }
    860         this.updateLayers();
     1010        this.updateLayers(true);
    8611011    }
    8621012
    8631013    /**
     
    9041054
    9051055    @Override
    9061056    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);
     1057        if (e.getRemovedLayer() instanceof IGeoImageLayer && ((IGeoImageLayer) e.getRemovedLayer()).containsImage(this.currentEntry)) {
     1058            displayImages(null);
    9131059        }
    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());
     1060        this.updateLayers(true);
    9161061    }
    9171062
    9181063    @Override
    9191064    public void layerOrderChanged(LayerOrderChangeEvent e) {
    920         // ignored
     1065        this.updateLayers(true);
    9211066    }
    9221067
    9231068    @Override
     
    9411086    }
    9421087
    9431088    private void registerOnLayer(Layer layer) {
    944         if (layer instanceof GeoImageLayer) {
    945             ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
     1089        if (layer instanceof IGeoImageLayer) {
     1090            layer.addPropertyChangeListener(l -> {
     1091                final List<?> currentTabLayers = this.getImageTabs().map(m -> m.layer).collect(Collectors.toList());
     1092                if (Layer.NAME_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
     1093                    this.updateLayers(true);
     1094                        if (((IGeoImageLayer) layer).containsImage(this.currentEntry)) {
     1095                            this.updateTitle();
     1096                        }
     1097                } // Use Layer.VISIBLE_PROP here if we decide to do something when layer visibility changes
     1098            });
    9461099        }
    9471100    }
    9481101
     
    9511104            ImageData imageData = ((GeoImageLayer) newLayer).getImageData();
    9521105            imageData.setSelectedImage(imageData.getFirstImage());
    9531106        }
     1107        if (newLayer instanceof IGeoImageLayer) {
     1108            this.updateLayers(true);
     1109        }
    9541110    }
    9551111
    9561112    private void cancelLoadingImage() {
     
    9591115            imgLoadingFuture = null;
    9601116        }
    9611117    }
    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     }
    9751118}
  • 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}