Ticket #21605: 21605.7.patch
File 21605.7.patch, 56.1 KB (added by , 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 33 33 34 34 import javax.swing.Action; 35 35 import javax.swing.Icon; 36 import javax.swing.ImageIcon; 36 37 37 38 import org.openstreetmap.josm.actions.AutoScaleAction; 38 39 import org.openstreetmap.josm.actions.ExpertToggleAction; … … 47 48 import org.openstreetmap.josm.data.gpx.GpxData; 48 49 import org.openstreetmap.josm.data.gpx.GpxImageEntry; 49 50 import org.openstreetmap.josm.data.gpx.GpxTrack; 51 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 50 52 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 53 import org.openstreetmap.josm.data.preferences.NamedColorProperty; 51 54 import org.openstreetmap.josm.gui.MainApplication; 52 55 import org.openstreetmap.josm.gui.MapFrame; 53 56 import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener; … … 62 65 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker; 63 66 import org.openstreetmap.josm.gui.layer.Layer; 64 67 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 68 import org.openstreetmap.josm.gui.util.GuiHelper; 65 69 import org.openstreetmap.josm.gui.util.imagery.Vector3D; 66 70 import org.openstreetmap.josm.tools.ImageProvider; 71 import org.openstreetmap.josm.tools.ListenerList; 67 72 import org.openstreetmap.josm.tools.Utils; 68 73 69 74 /** … … 71 76 * @since 99 72 77 */ 73 78 public class GeoImageLayer extends AbstractModifiableLayer implements 74 JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener { 79 JumpToMarkerLayer, NavigatableComponent.ZoomChangeListener, ImageDataUpdateListener, 80 IGeoImageLayer { 75 81 76 82 private static final List<Action> menuAdditions = new LinkedList<>(); 77 83 78 84 private static volatile List<MapMode> supportedMapModes; 79 85 80 86 private final ImageData data; 87 private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListeners = ListenerList.create(); 81 88 GpxData gpxData; 82 89 GpxLayer gpxFauxLayer; 83 90 GpxData gpxFauxData; … … 86 93 87 94 private final Icon icon = ImageProvider.get("dialogs/geoimage/photo-marker"); 88 95 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 } 89 109 90 110 boolean useThumbs; 91 111 private final ExecutorService thumbsLoaderExecutor = … … 172 192 this.useThumbs = useThumbs; 173 193 this.data.addImageDataUpdateListener(this); 174 194 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 } 175 203 } 176 204 177 205 private final class ImageMouseListener extends MouseAdapter { … … 232 260 } 233 261 } else { 234 262 data.setSelectedImage(img); 235 ImageViewerDialog.getInstance().displayImages( GeoImageLayer.this,Collections.singletonList(img));263 ImageViewerDialog.getInstance().displayImages(Collections.singletonList(img)); 236 264 } 265 GeoImageLayer.this.invalidate(); // Needed to update which image is being shown in the image viewer in the mapview 237 266 } 238 267 } 239 268 } … … 247 276 MainApplication.worker.execute(new ImagesLoader(files, gpxLayer)); 248 277 } 249 278 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 250 292 @Override 251 293 public Icon getIcon() { 252 294 return ImageProvider.get("dialogs/geoimage", ImageProvider.ImageSizes.LAYER); 253 295 } 254 296 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 255 318 /** 256 319 * Register actions on the layer 257 320 * @param addition the action to be added … … 451 514 } 452 515 } 453 516 517 final IImageEntry<?> currentImage = ImageViewerDialog.getCurrentImage(); 454 518 for (ImageEntry e: data.getSelectedImages()) { 455 519 if (e != null && e.getPos() != null) { 456 520 Point p = mv.getPoint(e.getPos()); … … 465 529 if (useThumbs && e.hasThumbnail()) { 466 530 g.setColor(new Color(128, 0, 0, 122)); 467 531 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); 468 536 } else { 469 selectedIcon .paintIcon(mv, g,537 selectedIconNotImageViewer.paintIcon(mv, g, 470 538 p.x - imgDim.width / 2, 471 539 p.y - imgDim.height / 2); 472 540 } … … 884 952 @Override 885 953 public void selectedImageChanged(ImageData data) { 886 954 showCurrentPhoto(); 955 this.imageChangeListeners.fireEvent(e -> e.imageChanged(this, null, data.getSelectedImages())); 887 956 } 888 957 889 958 @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. 2 package org.openstreetmap.josm.gui.layer.geoimage; 3 4 import java.util.Collections; 5 import java.util.List; 6 7 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 8 9 /** 10 * An interface for layers which want to show images 11 * @since xxx 12 */ 13 public 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 147 147 if (entry instanceof ImageEntry) { 148 148 this.dataSet.setSelectedImage((ImageEntry) entry); 149 149 } 150 imageViewerDialog.displayImages( this.dataSet.getLayer(),Collections.singletonList(entry));150 imageViewerDialog.displayImages(Collections.singletonList(entry)); 151 151 } 152 152 153 153 @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 14 14 import java.awt.event.ActionListener; 15 15 import java.awt.event.KeyEvent; 16 16 import java.awt.event.WindowEvent; 17 import java.beans.PropertyChangeEvent; 18 import java.beans.PropertyChangeListener; 17 19 import java.io.IOException; 18 20 import java.io.Serializable; 19 21 import java.time.ZoneOffset; … … 23 25 import java.util.Arrays; 24 26 import java.util.Collections; 25 27 import java.util.Comparator; 26 import java.util.HashMap;27 28 import java.util.List; 28 import java.util.Map;29 29 import java.util.Objects; 30 30 import java.util.Optional; 31 31 import java.util.concurrent.Future; 32 32 import java.util.function.UnaryOperator; 33 33 import java.util.stream.Collectors; 34 import java.util.stream.IntStream; 35 import java.util.stream.Stream; 34 36 35 37 import javax.swing.AbstractAction; 38 import javax.swing.AbstractButton; 39 import javax.swing.BorderFactory; 36 40 import javax.swing.Box; 37 41 import javax.swing.JButton; 38 42 import javax.swing.JLabel; 39 43 import javax.swing.JOptionPane; 40 44 import javax.swing.JPanel; 45 import javax.swing.JTabbedPane; 41 46 import javax.swing.JToggleButton; 42 47 import javax.swing.SwingConstants; 43 48 import javax.swing.SwingUtilities; 44 49 50 import org.openstreetmap.josm.actions.ExpertToggleAction; 45 51 import org.openstreetmap.josm.actions.JosmAction; 46 52 import org.openstreetmap.josm.data.ImageData; 47 import org.openstreetmap.josm.data.ImageData.ImageDataUpdateListener;48 53 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 49 54 import org.openstreetmap.josm.gui.ExtendedDialog; 50 55 import org.openstreetmap.josm.gui.MainApplication; … … 64 69 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 65 70 import org.openstreetmap.josm.gui.util.GuiHelper; 66 71 import org.openstreetmap.josm.gui.util.imagery.Vector3D; 72 import org.openstreetmap.josm.gui.widgets.HideableTabbedPane; 73 import org.openstreetmap.josm.spi.preferences.Config; 67 74 import org.openstreetmap.josm.tools.ImageProvider; 68 75 import org.openstreetmap.josm.tools.Logging; 69 76 import org.openstreetmap.josm.tools.PlatformManager; … … 73 80 /** 74 81 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}. 75 82 */ 76 public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener , ImageDataUpdateListener{83 public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener { 77 84 private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}"); 78 85 private static final String DIALOG_FOLDER = "dialogs"; 79 86 … … 124 131 return dialog; 125 132 } 126 133 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 127 150 private JButton btnLast; 128 151 private JButton btnNext; 129 152 private JButton btnPrevious; … … 135 158 private JButton btnDeleteFromDisk; 136 159 private JToggleButton tbCentre; 137 160 /** 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(); 139 162 140 163 private ImageViewerDialog() { 141 164 super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged", … … 168 191 169 192 private void build() { 170 193 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); 175 195 176 196 Dimension buttonDim = new Dimension(26, 26); 177 197 … … 187 207 btnLast = createNavigationButton(imageLastAction, buttonDim); 188 208 189 209 tbCentre = new JToggleButton(imageCenterViewAction); 210 tbCentre.setSelected(Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false)); 190 211 tbCentre.setPreferredSize(buttonDim); 191 212 192 213 JButton btnZoomBestFit = new JButton(imageZoomAction); … … 196 217 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT); 197 218 198 219 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)); 214 225 215 226 JPanel bottomPane = new JPanel(new GridBagLayout()); 216 227 GridBagConstraints gc = new GridBagConstraints(); … … 231 242 createLayout(content, false, null); 232 243 } 233 244 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()) { 236 269 this.layers.setVisible(false); 237 this.layers.removeAll();238 270 } else { 239 271 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 } 250 283 this.layers.invalidate(); 251 284 } 285 this.layers.getParent().invalidate(); 252 286 this.revalidate(); 253 287 } 254 288 … … 256 290 * Add the buttons for image layers 257 291 */ 258 292 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 } 269 336 } 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 } 276 363 } 277 layerButtons.forEach(this.layers::add);278 364 } 279 365 280 366 /** 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 286 369 */ 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); 292 375 } 293 376 294 377 @Override … … 309 392 imageZoomAction.destroy(); 310 393 cancelLoadingImage(); 311 394 super.destroy(); 312 d ialog = null;395 destroyInstance(); 313 396 } 314 397 315 398 /** … … 433 516 } 434 517 } 435 518 436 /**437 * A listener that is called to change the viewing layer438 */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 @Override450 public void actionPerformed(ActionEvent e) {451 ImageViewerDialog.getInstance().displayImages(this.layer, this.entries);452 }453 }454 455 519 private class ImageFirstAction extends ImageRememberAction { 456 520 ImageFirstAction() { 457 521 super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut( … … 478 542 public void actionPerformed(ActionEvent e) { 479 543 final JToggleButton button = (JToggleButton) e.getSource(); 480 544 centerView = button.isEnabled() && button.isSelected(); 545 Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView); 481 546 if (centerView && currentEntry != null && currentEntry.getPos() != null) { 482 547 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos()); 483 548 } … … 618 683 } 619 684 } 620 685 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 621 773 /** 622 774 * Enables (or disables) the "Previous" button. 623 775 * @param value {@code true} to enable the button, {@code false} otherwise … … 651 803 return wasEnabled; 652 804 } 653 805 654 /** Used for tabbed panes */655 private final transient Map<Layer, List<IImageEntry<?>>> tabbedEntries = new HashMap<>();656 806 private transient IImageEntry<? extends IImageEntry<?>> currentEntry; 657 807 658 808 /** … … 679 829 * @param entries image entries 680 830 * @since 18246 681 831 */ 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) { 693 833 boolean imageChanged; 694 834 IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null; 695 835 … … 710 850 } 711 851 } 712 852 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); 715 863 } else { 716 this.tabbedEntries.put(layer, entries);864 updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals)); 717 865 } 718 this.updateLayers( );866 this.updateLayers(updateRequired); 719 867 if (entry != null) { 720 868 this.updateButtonsNonNullEntry(entry, imageChanged); 721 } else if ( this.tabbedEntries.isEmpty()) {869 } else if (imageLayers.isEmpty()) { 722 870 this.updateButtonsNullEntry(entries); 723 871 return; 724 872 } 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) { 728 875 this.updateButtonsNullEntry(entries); 729 876 } else { 730 this.displayImages( realEntry.getKey(), realEntry.getValue());877 this.displayImages(layer.getSelection()); 731 878 } 732 879 return; 733 880 } … … 744 891 * Update buttons for null entry 745 892 * @param entries {@code true} if multiple images are selected 746 893 */ 747 private void updateButtonsNullEntry(List< IImageEntry<?>> entries) {894 private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) { 748 895 boolean hasMultipleImages = entries != null && entries.size() > 1; 749 896 // if this method is called to reinitialize dialog content with a blank image, 750 897 // do not actually show the dialog again with a blank image if currently hidden (fix #10672) 751 setTitle(tr("Geotagged Images"));898 this.updateTitle(); 752 899 imgDisplay.setImage(null); 753 900 imgDisplay.setOsdText(""); 754 901 setNextEnabled(false); … … 787 934 btnCopyPath.setEnabled(true); 788 935 btnOpenExternal.setEnabled(true); 789 936 790 setTitle(tr("Geotagged Images") + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));937 this.updateTitle(); 791 938 StringBuilder osd = new StringBuilder(entry.getDisplayName()); 792 939 if (entry.getElevation() != null) { 793 940 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation()))); … … 818 965 imgDisplay.setOsdText(osd.toString()); 819 966 } 820 967 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 } 831 981 } 832 982 833 private static boolean isLastImageSelected(List< IImageEntry<?>> data) {983 private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) { 834 984 return data.stream().anyMatch(image -> data.contains(image.getLastImage())); 835 985 } 836 986 837 private static boolean isFirstImageSelected(List< IImageEntry<?>> data) {987 private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) { 838 988 return data.stream().anyMatch(image -> data.contains(image.getFirstImage())); 839 989 } 840 990 … … 857 1007 if (btnCollapse != null) { 858 1008 btnCollapse.setVisible(!isDocked); 859 1009 } 860 this.updateLayers( );1010 this.updateLayers(true); 861 1011 } 862 1012 863 1013 /** … … 904 1054 905 1055 @Override 906 1056 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); 913 1059 } 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); 916 1061 } 917 1062 918 1063 @Override 919 1064 public void layerOrderChanged(LayerOrderChangeEvent e) { 920 // ignored1065 this.updateLayers(true); 921 1066 } 922 1067 923 1068 @Override … … 941 1086 } 942 1087 943 1088 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 }); 946 1099 } 947 1100 } 948 1101 … … 951 1104 ImageData imageData = ((GeoImageLayer) newLayer).getImageData(); 952 1105 imageData.setSelectedImage(imageData.getFirstImage()); 953 1106 } 1107 if (newLayer instanceof IGeoImageLayer) { 1108 this.updateLayers(true); 1109 } 954 1110 } 955 1111 956 1112 private void cancelLoadingImage() { … … 959 1115 imgLoadingFuture = null; 960 1116 } 961 1117 } 962 963 @Override964 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 @Override972 public void imageDataUpdated(ImageData data) {973 displayImages(data.getLayer(), new ArrayList<>(data.getSelectedImages()));974 }975 1118 } -
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 19 19 import org.openstreetmap.josm.data.coor.ILatLon; 20 20 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 21 21 import org.openstreetmap.josm.data.imagery.street_level.Projections; 22 import org.openstreetmap.josm.gui.layer.Layer;23 22 import org.openstreetmap.josm.tools.HttpClient; 24 23 import org.openstreetmap.josm.tools.JosmRuntimeException; 25 24 … … 33 32 private final Supplier<RemoteEntry> nextImage; 34 33 private final Supplier<RemoteEntry> previousImage; 35 34 private final Supplier<RemoteEntry> lastImage; 36 private final Layer layer;37 35 private int width; 38 36 private int height; 39 37 private ILatLon pos; … … 54 52 55 53 /** 56 54 * Create a new remote entry 57 * @param layer The originating layer, used for tabs in the image viewer58 55 * @param uri The URI to use 59 56 * @param firstImage first image supplier 60 57 * @param nextImage next image supplier 61 58 * @param lastImage last image supplier 62 59 * @param previousImage previous image supplier 63 60 */ 64 public RemoteEntry( Layer layer,URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage,61 public RemoteEntry(URI uri, Supplier<RemoteEntry> firstImage, Supplier<RemoteEntry> previousImage, 65 62 Supplier<RemoteEntry> nextImage, Supplier<RemoteEntry> lastImage) { 66 63 Objects.requireNonNull(uri); 67 64 Objects.requireNonNull(firstImage); … … 73 70 this.previousImage = previousImage; 74 71 this.nextImage = nextImage; 75 72 this.lastImage = lastImage; 76 this.layer = layer;77 73 } 78 74 79 75 @Override … … 319 315 320 316 @Override 321 317 public void selectImage(ImageViewerDialog imageViewerDialog, IImageEntry<?> entry) { 322 imageViewerDialog.displayImages( this.layer,Collections.singletonList(entry));318 imageViewerDialog.displayImages(Collections.singletonList(entry)); 323 319 } 324 320 325 321 @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 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 6 import java.awt.event.ActionEvent; 7 import java.net.URI; 7 8 import java.net.URISyntaxException; 8 9 import java.net.URL; 9 10 import java.time.Instant; … … 16 17 import org.openstreetmap.josm.data.gpx.GpxConstants; 17 18 import org.openstreetmap.josm.data.gpx.GpxLink; 18 19 import org.openstreetmap.josm.data.gpx.WayPoint; 20 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 19 21 import org.openstreetmap.josm.gui.Notification; 20 22 import org.openstreetmap.josm.gui.layer.geoimage.ImageViewerDialog; 21 23 import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry; … … 40 42 41 43 @Override 42 44 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())); 44 47 } 45 48 46 privateRemoteEntry getRemoteEntry() {49 RemoteEntry getRemoteEntry() { 47 50 try { 48 final RemoteEntry remoteEntry = new RemoteEntry(this.parentLayer,imageUrl.toURI(), getFirstImage(), getPreviousImage(),51 final RemoteEntry remoteEntry = new MarkerRemoteEntry(imageUrl.toURI(), getFirstImage(), getPreviousImage(), 49 52 getNextImage(), getLastImage()); 50 53 // First, extract EXIF data 51 54 remoteEntry.extractExif(); … … 128 131 wpt.put(GpxConstants.META_LINKS, Collections.singleton(link)); 129 132 return wpt; 130 133 } 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 } 131 156 } -
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 20 20 21 21 import javax.swing.ImageIcon; 22 22 23 import org.openstreetmap.josm.data.IQuadBucketType; 23 24 import org.openstreetmap.josm.data.Preferences; 24 25 import org.openstreetmap.josm.data.coor.CachedLatLon; 25 26 import org.openstreetmap.josm.data.coor.EastNorth; … … 27 28 import org.openstreetmap.josm.data.coor.LatLon; 28 29 import org.openstreetmap.josm.data.gpx.GpxConstants; 29 30 import org.openstreetmap.josm.data.gpx.WayPoint; 31 import org.openstreetmap.josm.data.osm.BBox; 30 32 import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 31 33 import org.openstreetmap.josm.gui.MapView; 32 34 import org.openstreetmap.josm.gui.layer.GpxLayer; … … 75 77 * 76 78 * @author Frederik Ramm 77 79 */ 78 public class Marker implements TemplateEngineDataProvider, ILatLon, Destroyable {80 public class Marker implements TemplateEngineDataProvider, ILatLon, Destroyable, IQuadBucketType { 79 81 80 82 /** 81 83 * Plugins can add their Marker creation stuff at the bottom or top of this list … … 447 449 private String getPreferenceKey() { 448 450 return "draw.rawgps." + getTextTemplateKey(); 449 451 } 452 453 @Override 454 public BBox getBBox() { 455 return new BBox(this); 456 } 450 457 } -
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 19 19 import java.net.URISyntaxException; 20 20 import java.util.ArrayList; 21 21 import java.util.Collection; 22 import java.util.Collections; 22 23 import java.util.Comparator; 23 24 import java.util.HashMap; 24 25 import java.util.List; 26 import java.util.ListIterator; 25 27 import java.util.Map; 28 import java.util.Objects; 26 29 import java.util.Optional; 27 30 28 31 import javax.swing.AbstractAction; … … 41 44 import org.openstreetmap.josm.data.gpx.GpxLink; 42 45 import org.openstreetmap.josm.data.gpx.IGpxLayerPrefs; 43 46 import org.openstreetmap.josm.data.gpx.WayPoint; 47 import org.openstreetmap.josm.data.imagery.street_level.IImageEntry; 48 import org.openstreetmap.josm.data.osm.BBox; 49 import org.openstreetmap.josm.data.osm.QuadBuckets; 44 50 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 45 51 import org.openstreetmap.josm.data.preferences.IntegerProperty; 46 52 import org.openstreetmap.josm.data.preferences.NamedColorProperty; … … 55 61 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToNextMarker; 56 62 import org.openstreetmap.josm.gui.layer.JumpToMarkerActions.JumpToPreviousMarker; 57 63 import org.openstreetmap.josm.gui.layer.Layer; 64 import org.openstreetmap.josm.gui.layer.geoimage.IGeoImageLayer; 65 import org.openstreetmap.josm.gui.layer.geoimage.RemoteEntry; 58 66 import org.openstreetmap.josm.gui.layer.gpx.ConvertFromMarkerLayerAction; 59 67 import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel; 60 68 import org.openstreetmap.josm.io.audio.AudioPlayer; 61 69 import org.openstreetmap.josm.spi.preferences.Config; 62 70 import org.openstreetmap.josm.tools.ColorHelper; 63 71 import org.openstreetmap.josm.tools.ImageProvider; 72 import org.openstreetmap.josm.tools.ListenerList; 64 73 import org.openstreetmap.josm.tools.Logging; 65 74 import org.openstreetmap.josm.tools.Utils; 66 75 … … 75 84 * 76 85 * The data is read only. 77 86 */ 78 public class MarkerLayer extends Layer implements JumpToMarkerLayer {87 public class MarkerLayer extends Layer implements JumpToMarkerLayer, IGeoImageLayer { 79 88 80 89 /** 81 90 * A list of markers. … … 89 98 final int markerSize = new IntegerProperty("draw.rawgps.markers.size", 4).get(); 90 99 final BasicStroke markerStroke = new StrokeProperty("draw.rawgps.markers.stroke", "1").get(); 91 100 101 private final ListenerList<IGeoImageLayer.ImageChangeListener> imageChangeListenerListenerList = ListenerList.create(); 102 92 103 /** 93 104 * The default color that is used for drawing markers. 94 105 */ … … 401 412 MainApplication.getMap().mapView.zoomTo(currentMarker); 402 413 } 403 414 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 404 423 public static void playAudio() { 405 424 playAdjacentMarker(null, true); 406 425 } … … 495 514 this.realcolor = Optional.ofNullable(color).orElse(DEFAULT_COLOR_PROPERTY.get()); 496 515 } 497 516 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 498 579 private final class MarkerMouseAdapter extends MouseAdapter { 499 580 @Override 500 581 public void mousePressed(MouseEvent e) { … … 627 708 * the data of a MarkerLayer 628 709 * @since 18287 629 710 */ 630 public class MarkerData extends ArrayList<Marker> implementsIGpxLayerPrefs {711 public class MarkerData extends QuadBuckets<Marker> implements List<Marker>, IGpxLayerPrefs { 631 712 632 713 private Map<String, String> ownLayerPrefs; 714 private final List<Marker> markerList = new ArrayList<>(); 633 715 634 716 @Override 635 717 public Map<String, String> getLayerPrefs() { … … 658 740 fromLayer.data.setModified(value); 659 741 } 660 742 } 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 } 661 814 } 662 815 }