Ticket #21605: 21605.2.patch

File 21605.2.patch, 17.0 KB (added by taylor.smock, 2 years ago)

Load icons in background thread, add reload method for plugins, add private class for action listeners for updating the image viewer

  • src/org/openstreetmap/josm/data/ImageData.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/ImageData.java b/src/org/openstreetmap/josm/data/ImageData.java
    a b  
    1010import org.openstreetmap.josm.data.coor.LatLon;
    1111import org.openstreetmap.josm.data.gpx.GpxImageEntry;
    1212import org.openstreetmap.josm.data.osm.QuadBuckets;
     13import org.openstreetmap.josm.gui.layer.Layer;
    1314import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
    1415import org.openstreetmap.josm.tools.ListenerList;
    1516
     
    4142
    4243    private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create();
    4344    private final QuadBuckets<ImageEntry> geoImages = new QuadBuckets<>();
     45    private Layer layer;
    4446
    4547    /**
    4648     * Construct a new image container without images
     
    375377        notifyImageUpdate();
    376378    }
    377379
     380    /**
     381     * Set the layer for use with {@link org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog#displayImages(Layer, List)}
     382     * @param layer The layer to use for organization
     383     * @since xxx
     384     */
     385    public void setLayer(Layer layer) {
     386        this.layer = layer;
     387    }
     388
     389    /**
     390     * Get the layer that this data is associated with. May be {@code null}.
     391     * @return The layer this data is associated with.
     392     * @since xxx
     393     */
     394    public Layer getLayer() {
     395        return this.layer;
     396    }
     397
    378398    /**
    379399     * Add a listener that listens to image data changes
    380400     * @param listener the {@link ImageDataUpdateListener}
  • src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java b/src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java
    a b  
    171171        this.gpxData = gpxData;
    172172        this.useThumbs = useThumbs;
    173173        this.data.addImageDataUpdateListener(this);
     174        this.data.setLayer(this);
    174175    }
    175176
    176177    private final class ImageMouseListener extends MouseAdapter {
     
    231232                    }
    232233                } else {
    233234                    data.setSelectedImage(img);
     235                    ImageViewerDialog.getInstance().displayImages(GeoImageLayer.this, Collections.singletonList(img));
    234236                }
    235237            }
    236238        }
     
    521523     * Show current photo on map and in image viewer.
    522524     */
    523525    public void showCurrentPhoto() {
    524         if (data.getSelectedImage() != null) {
    525             clearOtherCurrentPhotos();
    526         }
    527526        updateBufferAndRepaint();
    528527    }
    529528
     
    628627        }
    629628    }
    630629
    631     /**
    632      * Clears the currentPhoto of the other GeoImageLayer's. Otherwise there could be multiple selected photos.
    633      */
    634     private void clearOtherCurrentPhotos() {
    635         for (GeoImageLayer layer:
    636                  MainApplication.getLayerManager().getLayersOfType(GeoImageLayer.class)) {
    637             if (layer != this) {
    638                 layer.getImageData().clearSelectedImage();
    639             }
    640         }
    641     }
    642 
    643630    /**
    644631     * Registers a map mode for which the functionality of this layer should be available.
    645632     * @param mapMode Map mode to be registered
  • 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  
    88import java.awt.BorderLayout;
    99import java.awt.Component;
    1010import java.awt.Dimension;
     11import java.awt.FlowLayout;
    1112import java.awt.GridBagConstraints;
    1213import java.awt.GridBagLayout;
    1314import java.awt.event.ActionEvent;
     15import java.awt.event.ActionListener;
    1416import java.awt.event.KeyEvent;
    1517import java.awt.event.WindowEvent;
     18import java.awt.image.BufferedImage;
    1619import java.io.IOException;
    1720import java.io.Serializable;
     21import java.io.UncheckedIOException;
    1822import java.time.ZoneOffset;
    1923import java.time.format.DateTimeFormatter;
    2024import java.time.format.FormatStyle;
    2125import java.util.ArrayList;
    2226import java.util.Arrays;
    2327import java.util.Collections;
     28import java.util.Comparator;
     29import java.util.HashMap;
    2430import java.util.List;
     31import java.util.Map;
    2532import java.util.Objects;
    2633import java.util.Optional;
    2734import java.util.concurrent.Future;
    2835import java.util.function.UnaryOperator;
    2936import java.util.stream.Collectors;
     37import java.util.stream.Stream;
    3038
    3139import javax.swing.AbstractAction;
    3240import javax.swing.Box;
     41import javax.swing.ImageIcon;
    3342import javax.swing.JButton;
     43import javax.swing.JComponent;
    3444import javax.swing.JLabel;
    3545import javax.swing.JOptionPane;
    3646import javax.swing.JPanel;
    3747import javax.swing.JToggleButton;
    3848import javax.swing.SwingConstants;
     49import javax.swing.SwingUtilities;
    3950
    4051import org.openstreetmap.josm.actions.JosmAction;
    4152import org.openstreetmap.josm.data.ImageData;
     
    5263import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
    5364import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
    5465import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
     66import org.openstreetmap.josm.gui.layer.MainLayerManager;
    5567import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
    5668import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
    5769import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
     70import org.openstreetmap.josm.gui.util.GuiHelper;
    5871import org.openstreetmap.josm.gui.util.imagery.Vector3D;
    5972import org.openstreetmap.josm.tools.ImageProvider;
    6073import org.openstreetmap.josm.tools.Logging;
     
    6881public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener, ImageDataUpdateListener {
    6982    private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}");
    7083    private static final String DIALOG_FOLDER = "dialogs";
     84    private static final String JOSM_LAYER_COMPONENT = "JOSM LAYER";
     85    private static final String JOSM_LAYER_IMAGE_COMPONENT = "JOSM LAYER IImageEntry";
     86    private static final String JOSM_LAYER_IMAGE_COMPONENT_DONE = JOSM_LAYER_IMAGE_COMPONENT + ".done";
    7187
    7288    private final ImageryFilterSettings imageryFilterSettings = new ImageryFilterSettings();
    7389
     
    120136    private JButton btnOpenExternal;
    121137    private JButton btnDeleteFromDisk;
    122138    private JToggleButton tbCentre;
     139    /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */
     140    private JPanel layers;
    123141
    124142    private ImageViewerDialog() {
    125143        super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
     
    152170
    153171    private void build() {
    154172        JPanel content = new JPanel(new BorderLayout());
     173        this.layers = new JPanel(new FlowLayout(FlowLayout.LEADING));
     174        content.add(layers, BorderLayout.NORTH);
    155175
    156176        content.add(imgDisplay, BorderLayout.CENTER);
    157177
     
    213233        createLayout(content, false, null);
    214234    }
    215235
     236    private void updateLayers() {
     237        if (this.tabbedEntries.size() <= 1) {
     238            this.layers.setVisible(false);
     239        } else {
     240            final IImageEntry<?> current;
     241            synchronized (this) {
     242                current = this.currentEntry;
     243            }
     244            this.layers.setVisible(true);
     245            // Get the old components
     246            Map<Layer, JButton> oldLayers = Stream.of(this.layers.getComponents()).filter(JButton.class::isInstance).map(JButton.class::cast)
     247                    .filter(component -> component.getClientProperty(JOSM_LAYER_COMPONENT) instanceof Layer
     248                            || component.getClientProperty(JOSM_LAYER_COMPONENT) == null)
     249                    .collect(Collectors.toMap(component -> (Layer) component.getClientProperty(JOSM_LAYER_COMPONENT), component -> component));
     250            // Remove all old components
     251            this.layers.removeAll();
     252            List<JButton> layerButtons = new ArrayList<>(this.tabbedEntries.size());
     253            MainLayerManager layerManager = MainApplication.getLayerManager();
     254            List<Layer> invalidLayers = this.tabbedEntries.keySet().stream().filter(layer -> !layerManager.containsLayer(layer))
     255                    .collect(Collectors.toList());
     256            // `null` is for anything using the old methods, without telling us what layer it comes from.
     257            invalidLayers.remove(null);
     258            if (this.tabbedEntries.containsKey(null)) {
     259                List<IImageEntry<?>> nullEntries = this.tabbedEntries.get(null);
     260                JButton layerButton = createImageLayerButton(oldLayers, null, nullEntries);
     261                layerButtons.add(layerButton);
     262                layerButton.setEnabled(!nullEntries.contains(current));
     263            }
     264            // We need to do multiple calls to avoid ConcurrentModificationExceptions
     265            invalidLayers.forEach(this.tabbedEntries::remove);
     266            for (Map.Entry<Layer, List<IImageEntry<?>>> entry :
     267                    this.tabbedEntries.entrySet().stream().filter(entry -> entry.getKey() != null)
     268                            .sorted(Comparator.comparing(entry -> entry.getKey().getName())).collect(Collectors.toList())) {
     269                JButton layerButton = createImageLayerButton(oldLayers, entry.getKey(), entry.getValue());
     270                layerButtons.add(layerButton);
     271                layerButton.setEnabled(!entry.getValue().contains(current));
     272            }
     273            layerButtons.forEach(this.layers::add);
     274            int maxPreferredHeight = layerButtons.stream().mapToInt(JComponent::getHeight).max().orElse(Integer.MIN_VALUE);
     275            if (maxPreferredHeight > 0) {
     276                layerButtons.forEach(button -> button.setPreferredSize(new Dimension(button.getPreferredSize().width, maxPreferredHeight)));
     277            }
     278            this.layers.invalidate();
     279        }
     280    }
     281
     282    /**
     283     * Create a button for a specific layer and its entries
     284     *
     285     * @param oldLayers A map of old layers to {@link JButton}s. If a layer is in the map, there is at least one old {@link JButton}.
     286     * @param layer     The layer to switch to
     287     * @param entries   The entries to display
     288     * @return The button to use to switch to the specified layer
     289     */
     290    private JButton createImageLayerButton(Map<Layer, JButton> oldLayers, Layer layer, List<IImageEntry<?>> entries) {
     291        final JButton layerButton = new JButton(tr(layer != null ? layer.getLabel() : "Default"));
     292        layerButton.putClientProperty(JOSM_LAYER_COMPONENT, layer);
     293        layerButton.addActionListener(new ImageActionListener(layer, entries));
     294        if (!this.isDocked && entries.size() == 1) {
     295            IImageEntry<?> entry = entries.get(0);
     296            JButton old = oldLayers.get(layer);
     297            Object saved = Optional.ofNullable(old).map(button -> button.getClientProperty(JOSM_LAYER_IMAGE_COMPONENT)).orElse(null);
     298            boolean done = Optional.ofNullable(old).map(button -> button.getClientProperty(JOSM_LAYER_IMAGE_COMPONENT_DONE))
     299                    .map(Boolean.TRUE::equals).orElse(false);
     300            // Avoid reloading images if at all possible
     301            if (!Objects.equals(entry, saved) || !done) {
     302                ImageProvider.ImageSizes size = ImageProvider.ImageSizes.LARGEICON;
     303                layerButton.putClientProperty(JOSM_LAYER_IMAGE_COMPONENT, entry);
     304                layerButton.setIcon(ImageProvider.getEmpty(size));
     305                layerButton.setPreferredSize(new Dimension(layerButton.getPreferredSize().width + 2,
     306                        size.getAdjustedHeight()));
     307                MainApplication.worker.submit(() -> loadImage(layerButton, entry));
     308            } else {
     309                layerButton.putClientProperty(JOSM_LAYER_IMAGE_COMPONENT, old.getClientProperty(JOSM_LAYER_IMAGE_COMPONENT));
     310                layerButton.putClientProperty(JOSM_LAYER_IMAGE_COMPONENT_DONE, old.getClientProperty(JOSM_LAYER_IMAGE_COMPONENT_DONE));
     311                layerButton.setIcon(old.getIcon());
     312                layerButton.setPreferredSize(old.getPreferredSize());
     313            }
     314        }
     315        return layerButton;
     316    }
     317
     318    /**
     319     * Load an image for a button
     320     * @param layerButton The button to load the image into
     321     * @param iImageEntry The image entry to load
     322     */
     323    private static void loadImage(JButton layerButton, IImageEntry<?> iImageEntry) {
     324        try {
     325            BufferedImage image = iImageEntry.read(ImageProvider.ImageSizes.LARGEICON.getImageDimension());
     326            ImageIcon imageIcon = new ImageIcon(image);
     327            GuiHelper.runInEDT(() -> layerButton.setIcon(imageIcon));
     328            GuiHelper.runInEDT(layerButton::invalidate);
     329            layerButton.putClientProperty(JOSM_LAYER_IMAGE_COMPONENT_DONE, true);
     330        } catch (IOException e) {
     331            throw new UncheckedIOException(e);
     332        }
     333    }
     334
    216335    @Override
    217336    public void destroy() {
    218337        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
     
    352471        }
    353472    }
    354473
     474    /**
     475     * A listener that is called to change the viewing layer
     476     */
     477    private static class ImageActionListener implements ActionListener {
     478
     479        private final Layer layer;
     480        private final List<IImageEntry<?>> entries;
     481
     482        ImageActionListener(Layer layer, List<IImageEntry<?>> entries) {
     483            this.layer = layer;
     484            this.entries = entries;
     485        }
     486
     487        @Override
     488        public void actionPerformed(ActionEvent e) {
     489            ImageViewerDialog.getInstance().displayImages(this.layer, this.entries);
     490        }
     491    }
     492
     493
    355494    private class ImageFirstAction extends ImageRememberAction {
    356495        ImageFirstAction() {
    357496            super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
     
    551690        return wasEnabled;
    552691    }
    553692
     693    /** Used for tabbed panes */
     694    private final transient Map<Layer, List<IImageEntry<?>>> tabbedEntries = new HashMap<>();
    554695    private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
    555696
    556697    /**
     
    578719     * @since 18246
    579720     */
    580721    public void displayImages(List<IImageEntry<?>> entries) {
     722        this.displayImages((Layer) null, entries);
     723    }
     724
     725    /**
     726     * Displays images for the given layer.
     727     * @param layer The layer to use for the tab ui
     728     * @param entries image entries
     729     * @since xxx
     730     */
     731    public void displayImages(Layer layer, List<IImageEntry<?>> entries) {
    581732        boolean imageChanged;
    582733        IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
    583734
     
    598749            }
    599750        }
    600751
     752        if (entries == null || entries.isEmpty()) {
     753            this.tabbedEntries.remove(layer);
     754        } else {
     755            this.tabbedEntries.put(layer, entries);
     756        }
     757        this.updateLayers();
    601758        if (entry != null) {
    602759            this.updateButtonsNonNullEntry(entry, imageChanged);
    603760        } else {
     
    730887        if (btnCollapse != null) {
    731888            btnCollapse.setVisible(!isDocked);
    732889        }
     890        this.updateLayers();
    733891    }
    734892
    735893    /**
     
    797955        }
    798956    }
    799957
     958    /**
     959     * Reload the image. Call this if you load a low-resolution image first, and then get a high-resolution image, or
     960     * if you know that the image has changed on disk.
     961     * @since xxx
     962     */
     963    public void refresh() {
     964        if (SwingUtilities.isEventDispatchThread()) {
     965            this.updateButtonsNonNullEntry(currentEntry, true);
     966        } else {
     967            GuiHelper.runInEDT(this::refresh);
     968        }
     969    }
     970
    800971    private void registerOnLayer(Layer layer) {
    801972        if (layer instanceof GeoImageLayer) {
    802973            ((GeoImageLayer) layer).getImageData().addImageDataUpdateListener(this);
     
    819990
    820991    @Override
    821992    public void selectedImageChanged(ImageData data) {
    822         displayImages(new ArrayList<>(data.getSelectedImages()));
     993        displayImages(data.getLayer(), new ArrayList<>(data.getSelectedImages()));
    823994    }
    824995
    825996    @Override
    826997    public void imageDataUpdated(ImageData data) {
    827         displayImages(new ArrayList<>(data.getSelectedImages()));
     998        displayImages(data.getLayer(), new ArrayList<>(data.getSelectedImages()));
    828999    }
    8291000}