source: josm/trunk/src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java

Last change on this file was 19021, checked in by GerdP, 3 weeks ago

fix #23574: Preference geoimage.viewer.centre.on.image=true is ignored on startup

  • let the preference set the flag centerView instead of enabling the action only
  • Property svn:eol-style set to native
File size: 49.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.geoimage;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.BorderLayout;
9import java.awt.Component;
10import java.awt.Dimension;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.event.ActionEvent;
14import java.awt.event.ActionListener;
15import java.awt.event.KeyEvent;
16import java.awt.event.WindowEvent;
17import java.beans.PropertyChangeEvent;
18import java.beans.PropertyChangeListener;
19import java.io.IOException;
20import java.io.Serializable;
21import java.time.ZoneOffset;
22import java.time.format.DateTimeFormatter;
23import java.time.format.FormatStyle;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Collections;
27import java.util.Comparator;
28import java.util.List;
29import java.util.Objects;
30import java.util.Optional;
31import java.util.concurrent.Future;
32import java.util.function.UnaryOperator;
33import java.util.stream.Collectors;
34import java.util.stream.IntStream;
35import java.util.stream.Stream;
36
37import javax.swing.AbstractAction;
38import javax.swing.AbstractButton;
39import javax.swing.BorderFactory;
40import javax.swing.Box;
41import javax.swing.JButton;
42import javax.swing.JLabel;
43import javax.swing.JOptionPane;
44import javax.swing.JPanel;
45import javax.swing.JTabbedPane;
46import javax.swing.JToggleButton;
47import javax.swing.SwingConstants;
48import javax.swing.SwingUtilities;
49
50import org.openstreetmap.josm.actions.ExpertToggleAction;
51import org.openstreetmap.josm.actions.JosmAction;
52import org.openstreetmap.josm.data.ImageData;
53import org.openstreetmap.josm.data.imagery.street_level.IImageEntry;
54import org.openstreetmap.josm.gui.ExtendedDialog;
55import org.openstreetmap.josm.gui.MainApplication;
56import org.openstreetmap.josm.gui.MapFrame;
57import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
58import org.openstreetmap.josm.gui.dialogs.DialogsPanel;
59import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
60import org.openstreetmap.josm.gui.dialogs.layer.LayerVisibilityAction;
61import org.openstreetmap.josm.gui.layer.Layer;
62import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
63import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
64import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
65import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
66import org.openstreetmap.josm.gui.layer.MainLayerManager;
67import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
68import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
69import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
70import org.openstreetmap.josm.gui.util.GuiHelper;
71import org.openstreetmap.josm.gui.util.imagery.Vector3D;
72import org.openstreetmap.josm.gui.widgets.HideableTabbedPane;
73import org.openstreetmap.josm.spi.preferences.Config;
74import org.openstreetmap.josm.tools.ImageProvider;
75import org.openstreetmap.josm.tools.Logging;
76import org.openstreetmap.josm.tools.PlatformManager;
77import org.openstreetmap.josm.tools.Shortcut;
78import org.openstreetmap.josm.tools.date.DateUtils;
79
80/**
81 * Dialog to view and manipulate geo-tagged images from a {@link GeoImageLayer}.
82 */
83public final class ImageViewerDialog extends ToggleDialog implements LayerChangeListener, ActiveLayerChangeListener {
84 private static final String GEOIMAGE_FILLER = marktr("Geoimage: {0}");
85 private static final String DIALOG_FOLDER = "dialogs";
86
87 private final ImageryFilterSettings imageryFilterSettings = new ImageryFilterSettings();
88
89 private final ImageZoomAction imageZoomAction = new ImageZoomAction();
90 private final ImageCenterViewAction imageCenterViewAction = new ImageCenterViewAction();
91 private final ImageNextAction imageNextAction = new ImageNextAction();
92 private final ImageRemoveAction imageRemoveAction = new ImageRemoveAction();
93 private final ImageRemoveFromDiskAction imageRemoveFromDiskAction = new ImageRemoveFromDiskAction();
94 private final ImagePreviousAction imagePreviousAction = new ImagePreviousAction();
95 private final ImageCollapseAction imageCollapseAction = new ImageCollapseAction();
96 private final ImageFirstAction imageFirstAction = new ImageFirstAction();
97 private final ImageLastAction imageLastAction = new ImageLastAction();
98 private final ImageCopyPathAction imageCopyPathAction = new ImageCopyPathAction();
99 private final ImageOpenExternalAction imageOpenExternalAction = new ImageOpenExternalAction();
100 private final LayerVisibilityAction visibilityAction = new LayerVisibilityAction(Collections::emptyList,
101 () -> Collections.singleton(imageryFilterSettings));
102
103 private final ImageDisplay imgDisplay = new ImageDisplay(imageryFilterSettings);
104 private Future<?> imgLoadingFuture;
105 private boolean centerView;
106
107 // Only one instance of that class is present at one time
108 private static volatile ImageViewerDialog dialog;
109
110 private boolean collapseButtonClicked;
111
112 static void createInstance() {
113 if (dialog != null)
114 throw new IllegalStateException("ImageViewerDialog instance was already created");
115 dialog = new ImageViewerDialog();
116 }
117
118 /**
119 * Replies the unique instance of this dialog
120 * @return the unique instance
121 */
122 public static ImageViewerDialog getInstance() {
123 MapFrame map = MainApplication.getMap();
124 synchronized (ImageViewerDialog.class) {
125 if (dialog == null)
126 createInstance();
127 if (map != null && map.getToggleDialog(ImageViewerDialog.class) == null) {
128 map.addToggleDialog(dialog);
129 }
130 }
131 return dialog;
132 }
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 18613
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 MapFrame map = MainApplication.getMap();
148 synchronized (ImageViewerDialog.class) {
149 if (dialog != null && map != null && map.getToggleDialog(ImageViewerDialog.class) != null) {
150 map.removeToggleDialog(dialog);
151 }
152 }
153 dialog = null;
154 }
155
156 private JButton btnLast;
157 private JButton btnNext;
158 private JButton btnPrevious;
159 private JButton btnFirst;
160 private JButton btnCollapse;
161 private JButton btnDelete;
162 private JButton btnCopyPath;
163 private JButton btnOpenExternal;
164 private JButton btnDeleteFromDisk;
165 private JToggleButton tbCentre;
166 /** The layer tab (used to select images when multiple layers provide images, makes for easy switching) */
167 private final HideableTabbedPane layers = new HideableTabbedPane();
168
169 private ImageViewerDialog() {
170 super(tr("Geotagged Images"), "geoimage", tr("Display geotagged images"), Shortcut.registerShortcut("tools:geotagged",
171 tr("Windows: {0}", tr("Geotagged Images")), KeyEvent.VK_Y, Shortcut.DIRECT), 200);
172 build();
173 MainApplication.getLayerManager().addActiveLayerChangeListener(this);
174 MainApplication.getLayerManager().addLayerChangeListener(this);
175 for (Layer l: MainApplication.getLayerManager().getLayers()) {
176 registerOnLayer(l);
177 }
178 // This listener gets called _prior to_ the reorder event. If we do not delay the execution of the
179 // model update, then the image will change instead of remaining the same.
180 this.layers.getModel().addChangeListener(l -> {
181 // We need to check to see whether or not the worker is shut down. See #22922 for details.
182 if (!MainApplication.worker.isShutdown() && this.isDialogShowing()) {
183 MainApplication.worker.execute(() -> GuiHelper.runInEDT(this::showNotify));
184 }
185 });
186 }
187
188 @Override
189 public void showNotify() {
190 super.showNotify();
191 Component selected = this.layers.getSelectedComponent();
192 if (selected instanceof MoveImgDisplayPanel) {
193 ((MoveImgDisplayPanel<?>) selected).fireModelUpdate();
194 }
195 }
196
197 private static JButton createButton(AbstractAction action, Dimension buttonDim) {
198 JButton btn = new JButton(action);
199 btn.setPreferredSize(buttonDim);
200 btn.addPropertyChangeListener("enabled", e -> action.setEnabled(Boolean.TRUE.equals(e.getNewValue())));
201 return btn;
202 }
203
204 private static JButton createNavigationButton(AbstractAction action, Dimension buttonDim) {
205 JButton btn = createButton(action, buttonDim);
206 btn.setEnabled(false);
207 action.addPropertyChangeListener(l -> {
208 if ("enabled".equals(l.getPropertyName())) {
209 btn.setEnabled(action.isEnabled());
210 }
211 });
212 return btn;
213 }
214
215 private void build() {
216 JPanel content = new JPanel(new BorderLayout());
217 content.add(this.layers, BorderLayout.CENTER);
218
219 Dimension buttonDim = new Dimension(26, 26);
220
221 btnFirst = createNavigationButton(imageFirstAction, buttonDim);
222 btnPrevious = createNavigationButton(imagePreviousAction, buttonDim);
223
224 btnDelete = createButton(imageRemoveAction, buttonDim);
225 btnDeleteFromDisk = createButton(imageRemoveFromDiskAction, buttonDim);
226 btnCopyPath = createButton(imageCopyPathAction, buttonDim);
227 btnOpenExternal = createButton(imageOpenExternalAction, buttonDim);
228
229 btnNext = createNavigationButton(imageNextAction, buttonDim);
230 btnLast = createNavigationButton(imageLastAction, buttonDim);
231
232 centerView = Config.getPref().getBoolean("geoimage.viewer.centre.on.image", false);
233 tbCentre = new JToggleButton(imageCenterViewAction);
234 tbCentre.setSelected(centerView);
235 tbCentre.setPreferredSize(buttonDim);
236
237 JButton btnZoomBestFit = new JButton(imageZoomAction);
238 btnZoomBestFit.setPreferredSize(buttonDim);
239
240 btnCollapse = createButton(imageCollapseAction, new Dimension(20, 20));
241 btnCollapse.setAlignmentY(Component.TOP_ALIGNMENT);
242
243 JPanel buttons = new JPanel();
244 addButtonGroup(buttons, this.btnFirst, this.btnPrevious, this.btnNext, this.btnLast);
245 addButtonGroup(buttons, this.tbCentre, btnZoomBestFit);
246 addButtonGroup(buttons, this.btnDelete, this.btnDeleteFromDisk);
247 addButtonGroup(buttons, this.btnCopyPath, this.btnOpenExternal);
248 addButtonGroup(buttons, createButton(visibilityAction, buttonDim));
249
250 JPanel bottomPane = new JPanel(new GridBagLayout());
251 GridBagConstraints gc = new GridBagConstraints();
252 gc.gridx = 0;
253 gc.gridy = 0;
254 gc.anchor = GridBagConstraints.CENTER;
255 gc.weightx = 1;
256 bottomPane.add(buttons, gc);
257
258 gc.gridx = 1;
259 gc.gridy = 0;
260 gc.anchor = GridBagConstraints.PAGE_END;
261 gc.weightx = 0;
262 bottomPane.add(btnCollapse, gc);
263
264 content.add(bottomPane, BorderLayout.SOUTH);
265
266 createLayout(content, false, null);
267 }
268
269 /**
270 * Add a button group to a panel
271 * @param buttonPanel The panel holding the buttons
272 * @param buttons The button group to add
273 */
274 private static void addButtonGroup(JPanel buttonPanel, AbstractButton... buttons) {
275 if (buttonPanel.getComponentCount() != 0) {
276 buttonPanel.add(Box.createRigidArea(new Dimension(7, 0)));
277 }
278
279 for (AbstractButton jButton : buttons) {
280 buttonPanel.add(jButton);
281 }
282 }
283
284 /**
285 * Update the tabs for the different image layers
286 * @param changed {@code true} if the tabs changed
287 */
288 private void updateLayers(boolean changed) {
289 MainLayerManager layerManager = MainApplication.getLayerManager();
290 List<IGeoImageLayer> geoImageLayers = layerManager.getLayers().stream()
291 .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
292 if (geoImageLayers.isEmpty()) {
293 this.layers.setVisible(false);
294 } else {
295 this.layers.setVisible(true);
296 if (changed) {
297 addButtonsForImageLayers();
298 }
299 MoveImgDisplayPanel<?> selected = (MoveImgDisplayPanel<?>) this.layers.getSelectedComponent();
300 if ((this.imgDisplay.getParent() == null || this.imgDisplay.getParent().getParent() == null)
301 && selected != null && selected.layer.containsImage(this.currentEntry)) {
302 selected.setVisible(selected.isVisible());
303 } else if (selected != null && !selected.layer.containsImage(this.currentEntry)) {
304 this.getImageTabs().filter(m -> m.layer.containsImage(this.currentEntry)).mapToInt(this.layers::indexOfComponent).findFirst()
305 .ifPresent(this.layers::setSelectedIndex);
306 } else if (selected == null) {
307 updateTitle();
308 }
309 this.layers.invalidate();
310 }
311 this.layers.getParent().invalidate();
312 this.revalidate();
313 }
314
315 /**
316 * Add the buttons for image layers
317 */
318 private void addButtonsForImageLayers() {
319 List<MoveImgDisplayPanel<?>> alreadyAdded = this.getImageTabs().collect(Collectors.toList());
320 List<Layer> availableLayers = MainApplication.getLayerManager().getLayers();
321 List<IGeoImageLayer> geoImageLayers = availableLayers.stream()
322 .sorted(Comparator.comparingInt(entry -> /*reverse*/-availableLayers.indexOf(entry)))
323 .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
324 List<IGeoImageLayer> tabLayers = geoImageLayers.stream()
325 .filter(l -> alreadyAdded.stream().anyMatch(m -> Objects.equals(l, m.layer)) || l.containsImage(this.currentEntry))
326 .collect(Collectors.toList());
327 for (IGeoImageLayer layer : tabLayers) {
328 final MoveImgDisplayPanel<?> panel = alreadyAdded.stream()
329 .filter(m -> Objects.equals(m.layer, layer)).findFirst()
330 .orElseGet(() -> new MoveImgDisplayPanel<>(this.imgDisplay, (Layer & IGeoImageLayer) layer));
331 int componentIndex = this.layers.indexOfComponent(panel);
332 if (componentIndex == geoImageLayers.indexOf(layer)) {
333 this.layers.setTitleAt(componentIndex, panel.getLabel(availableLayers));
334 } else {
335 this.removeImageTab((Layer) layer);
336 this.layers.insertTab(panel.getLabel(availableLayers), null, panel, null, tabLayers.indexOf(layer));
337 int idx = this.layers.indexOfComponent(panel);
338 CloseableTab closeableTab = new CloseableTab(this.layers, l -> {
339 Component source = (Component) l.getSource();
340 do {
341 int index = layers.indexOfTabComponent(source);
342 if (index >= 0) {
343 removeImageTab(((MoveImgDisplayPanel<?>) layers.getComponentAt(index)).layer);
344 getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
345 showNotify();
346 return;
347 }
348 source = source.getParent();
349 } while (source != null);
350 });
351 this.layers.setTabComponentAt(idx, closeableTab);
352 }
353 if (layer.containsImage(this.currentEntry)) {
354 this.layers.setSelectedComponent(panel);
355 }
356 }
357 this.getImageTabs().map(p -> p.layer).filter(layer -> !availableLayers.contains(layer))
358 // We have to collect to a list prior to removal -- if we don't, then the stream may get a layer at index 0,
359 // remove that layer, and then get a layer at index 1, which was previously at index 2.
360 .collect(Collectors.toList()).forEach(this::removeImageTab);
361
362 // After that, trigger the visibility set code
363 this.getImageTabs().forEach(m -> m.setVisible(m.isVisible()));
364 }
365
366 /**
367 * Remove a tab for a layer from the {@link #layers} tab pane
368 * @param layer The layer to remove
369 */
370 private void removeImageTab(Layer layer) {
371 // This must be reversed to avoid removing the wrong tab
372 for (int i = this.layers.getTabCount() - 1; i >= 0; i--) {
373 Component component = this.layers.getComponentAt(i);
374 if (component instanceof MoveImgDisplayPanel) {
375 MoveImgDisplayPanel<?> moveImgDisplayPanel = (MoveImgDisplayPanel<?>) component;
376 if (Objects.equals(layer, moveImgDisplayPanel.layer)) {
377 this.layers.removeTabAt(i);
378 this.layers.remove(moveImgDisplayPanel);
379 }
380 }
381 }
382 }
383
384 /**
385 * Get the {@link MoveImgDisplayPanel} objects in {@link #layers}.
386 * @return The individual panels
387 */
388 private Stream<MoveImgDisplayPanel<?>> getImageTabs() {
389 return IntStream.range(0, this.layers.getTabCount())
390 .mapToObj(this.layers::getComponentAt)
391 .filter(MoveImgDisplayPanel.class::isInstance)
392 .map(m -> (MoveImgDisplayPanel<?>) m);
393 }
394
395 @Override
396 public void destroy() {
397 MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
398 MainApplication.getLayerManager().removeLayerChangeListener(this);
399 // Manually destroy actions until JButtons are replaced by standard SideButtons
400 imageFirstAction.destroy();
401 imageLastAction.destroy();
402 imagePreviousAction.destroy();
403 imageNextAction.destroy();
404 imageCenterViewAction.destroy();
405 imageCollapseAction.destroy();
406 imageCopyPathAction.destroy();
407 imageOpenExternalAction.destroy();
408 imageRemoveAction.destroy();
409 imageRemoveFromDiskAction.destroy();
410 imageZoomAction.destroy();
411 toggleAction.destroy();
412 cancelLoadingImage();
413 super.destroy();
414 // make sure that Image Display is destroyed here, it might not be a component
415 imgDisplay.destroy();
416 // Ensure that this dialog is removed from memory
417 destroyInstance();
418 }
419
420 /**
421 * This literally exists to silence sonarlint complaints.
422 * @param <I> the type of the operand and result of the operator
423 */
424 @FunctionalInterface
425 private interface SerializableUnaryOperator<I> extends UnaryOperator<I>, Serializable {
426 }
427
428 private abstract class ImageAction extends JosmAction {
429 final SerializableUnaryOperator<IImageEntry<?>> supplier;
430 ImageAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut,
431 boolean registerInToolbar, String toolbarId, boolean installAdaptors,
432 final SerializableUnaryOperator<IImageEntry<?>> supplier) {
433 super(name, icon, tooltip, shortcut, registerInToolbar, toolbarId, installAdaptors);
434 Objects.requireNonNull(supplier);
435 this.supplier = supplier;
436 }
437
438 @Override
439 public void actionPerformed(ActionEvent event) {
440 final IImageEntry<?> entry = ImageViewerDialog.this.currentEntry;
441 if (entry != null) {
442 IImageEntry<?> nextEntry = this.getSupplier().apply(entry);
443 entry.selectImage(ImageViewerDialog.this, nextEntry);
444 }
445 this.resetRememberActions();
446 }
447
448 void resetRememberActions() {
449 for (ImageRememberAction action : Arrays.asList(ImageViewerDialog.this.imageLastAction, ImageViewerDialog.this.imageFirstAction)) {
450 action.last = null;
451 action.updateEnabledState();
452 }
453 }
454
455 SerializableUnaryOperator<IImageEntry<?>> getSupplier() {
456 return this.supplier;
457 }
458
459 @Override
460 protected void updateEnabledState() {
461 final IImageEntry<?> entry = ImageViewerDialog.this.currentEntry;
462 this.setEnabled(entry != null && this.getSupplier().apply(entry) != null);
463 }
464 }
465
466 private class ImageNextAction extends ImageAction {
467 ImageNextAction() {
468 super(null, new ImageProvider(DIALOG_FOLDER, "next"), tr("Next"), Shortcut.registerShortcut(
469 "geoimage:next", tr(GEOIMAGE_FILLER, tr("Show next Image")), KeyEvent.VK_PAGE_DOWN, Shortcut.DIRECT),
470 false, null, false, IImageEntry::getNextImage);
471 }
472 }
473
474 private class ImagePreviousAction extends ImageAction {
475 ImagePreviousAction() {
476 super(null, new ImageProvider(DIALOG_FOLDER, "previous"), tr("Previous"), Shortcut.registerShortcut(
477 "geoimage:previous", tr(GEOIMAGE_FILLER, tr("Show previous Image")), KeyEvent.VK_PAGE_UP, Shortcut.DIRECT),
478 false, null, false, IImageEntry::getPreviousImage);
479 }
480 }
481
482 /** This class exists to remember the last entry, and go back if clicked again when it would not otherwise be enabled */
483 private abstract class ImageRememberAction extends ImageAction {
484 private final ImageProvider defaultIcon;
485 transient IImageEntry<?> last;
486 ImageRememberAction(String name, ImageProvider icon, String tooltip, Shortcut shortcut,
487 boolean registerInToolbar, String toolbarId, boolean installAdaptors, SerializableUnaryOperator<IImageEntry<?>> supplier) {
488 super(name, icon, tooltip, shortcut, registerInToolbar, toolbarId, installAdaptors, supplier);
489 this.defaultIcon = icon;
490 }
491
492 /**
493 * Update the icon for this action
494 */
495 public void updateIcon() {
496 if (this.last != null) {
497 new ImageProvider(DIALOG_FOLDER, "history").getResource().attachImageIcon(this, true);
498 } else {
499 this.defaultIcon.getResource().attachImageIcon(this, true);
500 }
501 }
502
503 @Override
504 public void actionPerformed(ActionEvent event) {
505 final IImageEntry<?> current = ImageViewerDialog.this.currentEntry;
506 final IImageEntry<?> expected = this.supplier.apply(current);
507 if (current != null) {
508 IImageEntry<?> nextEntry = this.getSupplier().apply(current);
509 current.selectImage(ImageViewerDialog.this, nextEntry);
510 }
511 this.resetRememberActions();
512 if (!Objects.equals(current, expected)) {
513 this.last = current;
514 } else {
515 this.last = null;
516 }
517 this.updateEnabledState();
518 }
519
520 @Override
521 protected void updateEnabledState() {
522 final IImageEntry<?> current = ImageViewerDialog.this.currentEntry;
523 final IImageEntry<?> nextEntry = current != null ? this.getSupplier().apply(current) : null;
524 if (this.last == null && nextEntry != null && nextEntry.equals(current)) {
525 this.setEnabled(false);
526 } else {
527 super.updateEnabledState();
528 }
529 this.updateIcon();
530 }
531
532 @Override
533 SerializableUnaryOperator<IImageEntry<?>> getSupplier() {
534 if (this.last != null) {
535 return entry -> this.last;
536 }
537 return super.getSupplier();
538 }
539 }
540
541 private class ImageFirstAction extends ImageRememberAction {
542 ImageFirstAction() {
543 super(null, new ImageProvider(DIALOG_FOLDER, "first"), tr("First"), Shortcut.registerShortcut(
544 "geoimage:first", tr(GEOIMAGE_FILLER, tr("Show first Image")), KeyEvent.VK_HOME, Shortcut.DIRECT),
545 false, null, false, IImageEntry::getFirstImage);
546 }
547 }
548
549 private class ImageLastAction extends ImageRememberAction {
550 ImageLastAction() {
551 super(null, new ImageProvider(DIALOG_FOLDER, "last"), tr("Last"), Shortcut.registerShortcut(
552 "geoimage:last", tr(GEOIMAGE_FILLER, tr("Show last Image")), KeyEvent.VK_END, Shortcut.DIRECT),
553 false, null, false, IImageEntry::getLastImage);
554 }
555 }
556
557 private class ImageCenterViewAction extends JosmAction {
558 ImageCenterViewAction() {
559 super(null, new ImageProvider("dialogs/autoscale", "selection"), tr("Center view"), null,
560 false, null, false);
561 }
562
563 @Override
564 public void actionPerformed(ActionEvent e) {
565 final JToggleButton button = (JToggleButton) e.getSource();
566 centerView = button.isEnabled() && button.isSelected();
567 Config.getPref().putBoolean("geoimage.viewer.centre.on.image", centerView);
568 if (centerView && currentEntry != null && currentEntry.getPos() != null) {
569 MainApplication.getMap().mapView.zoomTo(currentEntry.getPos());
570 }
571 }
572 }
573
574 private class ImageZoomAction extends JosmAction {
575 ImageZoomAction() {
576 super(null, new ImageProvider(DIALOG_FOLDER, "zoom-best-fit"), tr("Zoom best fit and 1:1"), null,
577 false, null, false);
578 }
579
580 @Override
581 public void actionPerformed(ActionEvent e) {
582 imgDisplay.zoomBestFitOrOne();
583 }
584 }
585
586 private class ImageRemoveAction extends JosmAction {
587 ImageRemoveAction() {
588 super(null, new ImageProvider(DIALOG_FOLDER, "delete"), tr("Remove photo from layer"), Shortcut.registerShortcut(
589 "geoimage:deleteimagefromlayer", tr(GEOIMAGE_FILLER, tr("Remove photo from layer")), KeyEvent.VK_DELETE, Shortcut.SHIFT),
590 false, null, false);
591 }
592
593 @Override
594 public void actionPerformed(ActionEvent e) {
595 if (ImageViewerDialog.this.currentEntry != null) {
596 IImageEntry<?> imageEntry = ImageViewerDialog.this.currentEntry;
597 if (imageEntry.isRemoveSupported()) {
598 imageEntry.remove();
599 }
600 selectNextImageAfterDeletion(imageEntry);
601 }
602 }
603
604 /**
605 * Select the logical next entry after deleting the currently viewed image
606 * @param oldEntry The image entry that was just deleted
607 */
608 private void selectNextImageAfterDeletion(IImageEntry<?> oldEntry) {
609 final IImageEntry<?> currentImageEntry = ImageViewerDialog.this.currentEntry;
610 // This is mostly just in case something changes the displayed entry (aka avoid race condition) or an image provider
611 // sets the next image itself.
612 if (Objects.equals(currentImageEntry, oldEntry)) {
613 final IImageEntry<?> nextImage;
614 if (oldEntry instanceof ImageEntry) {
615 nextImage = ((ImageEntry) oldEntry).getDataSet().getSelectedImage();
616 } else if (oldEntry.getNextImage() != null) {
617 nextImage = oldEntry.getNextImage();
618 } else if (oldEntry.getPreviousImage() != null) {
619 nextImage = oldEntry.getPreviousImage();
620 } else {
621 nextImage = null;
622 }
623 ImageViewerDialog.getInstance().displayImages(nextImage == null ? null : Collections.singletonList(nextImage));
624 }
625 }
626 }
627
628 private class ImageRemoveFromDiskAction extends JosmAction {
629 ImageRemoveFromDiskAction() {
630 super(null, new ImageProvider(DIALOG_FOLDER, "geoimage/deletefromdisk"), tr("Delete image file from disk"),
631 Shortcut.registerShortcut("geoimage:deletefilefromdisk",
632 tr(GEOIMAGE_FILLER, tr("Delete image file from disk")), KeyEvent.VK_DELETE, Shortcut.CTRL_SHIFT),
633 false, null, false);
634 }
635
636 @Override
637 public void actionPerformed(ActionEvent e) {
638 if (currentEntry != null) {
639 IImageEntry<?> oldEntry = ImageViewerDialog.this.currentEntry;
640 List<IImageEntry<?>> toDelete = oldEntry instanceof ImageEntry ?
641 new ArrayList<>(((ImageEntry) oldEntry).getDataSet().getSelectedImages())
642 : Collections.singletonList(oldEntry);
643 int size = toDelete.size();
644
645 int result = new ExtendedDialog(
646 MainApplication.getMainFrame(),
647 tr("Delete image file from disk"),
648 tr("Cancel"), tr("Delete"))
649 .setButtonIcons("cancel", "dialogs/geoimage/deletefromdisk")
650 .setContent(new JLabel("<html><h3>"
651 + trn("Delete the file from disk?",
652 "Delete the {0} files from disk?", size, size)
653 + "<p>" + trn("The image file will be permanently lost!",
654 "The images files will be permanently lost!", size) + "</h3></html>",
655 ImageProvider.get("dialogs/geoimage/deletefromdisk"), SwingConstants.LEADING))
656 .toggleEnable("geoimage.deleteimagefromdisk")
657 .setCancelButton(1)
658 .setDefaultButton(2)
659 .showDialog()
660 .getValue();
661
662 if (result == 2) {
663 final List<ImageData> imageDataCollection = toDelete.stream().filter(ImageEntry.class::isInstance)
664 .map(ImageEntry.class::cast).map(ImageEntry::getDataSet).distinct().collect(Collectors.toList());
665 for (IImageEntry<?> delete : toDelete) {
666 // We have to be able to remove the image from the layer and the image from its storage location
667 // If either are false, then don't remove the image.
668 if (delete.isRemoveSupported() && delete.isDeleteSupported() && delete.remove() && delete.delete()) {
669 Logging.info("File {0} deleted.", delete.getFile());
670 } else {
671 JOptionPane.showMessageDialog(
672 MainApplication.getMainFrame(),
673 tr("Image file could not be deleted."),
674 tr("Error"),
675 JOptionPane.ERROR_MESSAGE
676 );
677 }
678 }
679 imageDataCollection.forEach(data -> {
680 data.notifyImageUpdate();
681 data.updateSelectedImage();
682 });
683 ImageViewerDialog.this.imageRemoveAction.selectNextImageAfterDeletion(oldEntry);
684 }
685 }
686 }
687 }
688
689 private class ImageCopyPathAction extends JosmAction {
690 ImageCopyPathAction() {
691 super(null, new ImageProvider("copy"), tr("Copy image path"), Shortcut.registerShortcut(
692 "geoimage:copypath", tr(GEOIMAGE_FILLER, tr("Copy image path")), KeyEvent.VK_C, Shortcut.ALT_CTRL_SHIFT),
693 false, null, false);
694 }
695
696 @Override
697 public void actionPerformed(ActionEvent e) {
698 if (currentEntry != null) {
699 ClipboardUtils.copyString(String.valueOf(currentEntry.getImageURI()));
700 }
701 }
702 }
703
704 private class ImageCollapseAction extends JosmAction {
705 ImageCollapseAction() {
706 super(null, new ImageProvider(DIALOG_FOLDER, "collapse"), tr("Move dialog to the side pane"), null,
707 false, null, false);
708 }
709
710 @Override
711 public void actionPerformed(ActionEvent e) {
712 collapseButtonClicked = true;
713 detachedDialog.getToolkit().getSystemEventQueue().postEvent(new WindowEvent(detachedDialog, WindowEvent.WINDOW_CLOSING));
714 }
715 }
716
717 private class ImageOpenExternalAction extends JosmAction {
718 ImageOpenExternalAction() {
719 super(null, new ImageProvider("external-link"), tr("Open image in external viewer"), null, false, null, false);
720 }
721
722 @Override
723 public void actionPerformed(ActionEvent e) {
724 if (currentEntry != null && currentEntry.getImageURI() != null) {
725 try {
726 PlatformManager.getPlatform().openUrl(currentEntry.getImageURI().toURL().toExternalForm());
727 } catch (IOException ex) {
728 Logging.error(ex);
729 }
730 }
731 }
732 }
733
734 /**
735 * A tab title renderer for {@link HideableTabbedPane} that allows us to close tabs.
736 */
737 private static class CloseableTab extends JPanel implements PropertyChangeListener {
738 private final JLabel title;
739 private final JButton close;
740
741 /**
742 * Create a new {@link CloseableTab}.
743 * @param parent The parent to add property change listeners to. It should be a {@link HideableTabbedPane} in most cases.
744 * @param closeAction The action to run to close the tab. You probably want to call {@link JTabbedPane#removeTabAt(int)}
745 * at the very least.
746 */
747 CloseableTab(Component parent, ActionListener closeAction) {
748 this.title = new JLabel();
749 this.add(this.title);
750 close = new JButton(ImageProvider.get("misc", "close"));
751 close.setBorder(BorderFactory.createEmptyBorder());
752 this.add(close);
753 close.addActionListener(closeAction);
754 close.addActionListener(l -> parent.removePropertyChangeListener("indexForTitle", this));
755 parent.addPropertyChangeListener("indexForTitle", this);
756 }
757
758 @Override
759 public void propertyChange(PropertyChangeEvent evt) {
760 if (evt.getSource() instanceof JTabbedPane) {
761 JTabbedPane source = (JTabbedPane) evt.getSource();
762 if (this.getParent() == null) {
763 source.removePropertyChangeListener(evt.getPropertyName(), this);
764 }
765 if ("indexForTitle".equals(evt.getPropertyName())) {
766 int idx = source.indexOfTabComponent(this);
767 if (idx >= 0) {
768 this.title.setText(source.getTitleAt(idx));
769 }
770 }
771 // Used to hack around UI staying visible. This assumes that the parent component is a HideableTabbedPane.
772 this.title.setVisible(source.getTabCount() != 1);
773 this.close.setVisible(source.getTabCount() != 1);
774 }
775 }
776 }
777
778 /**
779 * A JPanel whose entire purpose is to display an image by (a) moving the imgDisplay around and (b) setting the imgDisplay as a child
780 * for this panel.
781 */
782 private static class MoveImgDisplayPanel<T extends Layer & IGeoImageLayer> extends JPanel {
783 private final T layer;
784 private final ImageDisplay imgDisplay;
785
786 MoveImgDisplayPanel(ImageDisplay imgDisplay, T layer) {
787 super(new BorderLayout());
788 this.layer = layer;
789 this.imgDisplay = imgDisplay;
790 }
791
792 /**
793 * Call when the selection model updates
794 */
795 void fireModelUpdate() {
796 JTabbedPane layers = ImageViewerDialog.getInstance().layers;
797 int index = layers.indexOfComponent(this);
798 if (this == layers.getSelectedComponent()) {
799 if (!this.layer.getSelection().isEmpty() && !this.layer.getSelection().contains(ImageViewerDialog.getCurrentImage())) {
800 ImageViewerDialog.getInstance().displayImages(this.layer.getSelection());
801 this.layer.invalidate(); // This will force the geoimage layers to update properly.
802 }
803 if (this.imgDisplay.getParent() != this) {
804 this.add(this.imgDisplay, BorderLayout.CENTER);
805 this.imgDisplay.invalidate();
806 this.revalidate();
807 }
808 if (index >= 0) {
809 layers.setTitleAt(index, "* " + getLabel(MainApplication.getLayerManager().getLayers()));
810 }
811 } else if (index >= 0) {
812 layers.setTitleAt(index, getLabel(MainApplication.getLayerManager().getLayers()));
813 }
814 }
815
816 /**
817 * Get the label for this panel
818 * @param availableLayers The layers to use to get the index
819 * @return The label for this layer
820 */
821 String getLabel(List<Layer> availableLayers) {
822 final int index = availableLayers.size() - availableLayers.indexOf(layer);
823 return (ExpertToggleAction.isExpert() ? "[" + index + "] " : "") + layer.getLabel();
824 }
825 }
826
827 /**
828 * Enables (or disables) the "Previous" button.
829 * @param value {@code true} to enable the button, {@code false} otherwise
830 */
831 public void setPreviousEnabled(boolean value) {
832 this.imageFirstAction.updateEnabledState();
833 this.btnFirst.setEnabled(value || this.imageFirstAction.isEnabled());
834 btnPrevious.setEnabled(value);
835 }
836
837 /**
838 * Enables (or disables) the "Next" button.
839 * @param value {@code true} to enable the button, {@code false} otherwise
840 */
841 public void setNextEnabled(boolean value) {
842 btnNext.setEnabled(value);
843 this.imageLastAction.updateEnabledState();
844 this.btnLast.setEnabled(value || this.imageLastAction.isEnabled());
845 }
846
847 /**
848 * Enables (or disables) the "Center view" button.
849 * @param value {@code true} to enable the button, {@code false} otherwise
850 * @return the old enabled value. Can be used to restore the original enable state
851 */
852 public static synchronized boolean setCentreEnabled(boolean value) {
853 final ImageViewerDialog instance = getInstance();
854 final boolean wasEnabled = instance.tbCentre.isEnabled();
855 instance.tbCentre.setEnabled(value);
856 instance.tbCentre.getAction().actionPerformed(new ActionEvent(instance.tbCentre, 0, null));
857 return wasEnabled;
858 }
859
860 private transient IImageEntry<? extends IImageEntry<?>> currentEntry;
861
862 /**
863 * Displays a single image for the given layer.
864 * @param ignoredData the image data
865 * @param entry image entry
866 * @see #displayImages
867 */
868 public void displayImage(ImageData ignoredData, ImageEntry entry) {
869 displayImages(Collections.singletonList(entry));
870 }
871
872 /**
873 * Displays a single image for the given layer.
874 * @param entry image entry
875 * @see #displayImages
876 */
877 public void displayImage(IImageEntry<?> entry) {
878 this.displayImages(Collections.singletonList(entry));
879 }
880
881 /**
882 * Displays images for the given layer.
883 * @param entries image entries
884 * @since 18246
885 */
886 public void displayImages(List<? extends IImageEntry<?>> entries) {
887 boolean imageChanged;
888 IImageEntry<?> entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
889
890 synchronized (this) {
891 // TODO: pop up image dialog but don't load image again
892
893 imageChanged = currentEntry != entry;
894
895 if (centerView && entry != null && MainApplication.isDisplayingMapView() && entry.getPos() != null) {
896 MainApplication.getMap().mapView.zoomTo(entry.getPos());
897 }
898
899 currentEntry = entry;
900
901 for (ImageAction action : Arrays.asList(this.imageFirstAction, this.imagePreviousAction,
902 this.imageNextAction, this.imageLastAction)) {
903 action.updateEnabledState();
904 }
905 }
906
907
908 final boolean updateRequired;
909 final List<IGeoImageLayer> imageLayers = MainApplication.getLayerManager().getLayers().stream()
910 .filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast).collect(Collectors.toList());
911 if (!Config.getPref().getBoolean("geoimage.viewer.show.tabs", true)) {
912 updateRequired = true;
913 layers.removeAll();
914 // Clear the selected images in other geoimage layers
915 this.getImageTabs().map(m -> m.layer).filter(IGeoImageLayer.class::isInstance).map(IGeoImageLayer.class::cast)
916 .filter(l -> !Objects.equals(entries, l.getSelection()))
917 .forEach(IGeoImageLayer::clearSelection);
918 } else {
919 updateRequired = imageLayers.stream().anyMatch(l -> this.getImageTabs().map(m -> m.layer).noneMatch(l::equals));
920 }
921 this.updateLayers(updateRequired);
922 if (entry != null) {
923 this.updateButtonsNonNullEntry(entry, imageChanged);
924 } else if (imageLayers.isEmpty()) {
925 this.updateButtonsNullEntry(entries);
926 return;
927 } else {
928 IGeoImageLayer layer = this.getImageTabs().map(m -> m.layer).filter(l -> l.getSelection().size() == 1).findFirst().orElse(null);
929 if (layer == null) {
930 this.updateButtonsNullEntry(entries);
931 } else {
932 this.displayImages(layer.getSelection());
933 }
934 return;
935 }
936 if (!isDialogShowing()) {
937 setIsDocked(false); // always open a detached window when an image is clicked and dialog is closed
938 unfurlDialog();
939 } else if (isDocked && isCollapsed) {
940 expand();
941 dialogsPanel.reconstruct(DialogsPanel.Action.COLLAPSED_TO_DEFAULT, this);
942 }
943 }
944
945 /**
946 * Update buttons for null entry
947 * @param entries {@code true} if multiple images are selected
948 */
949 private void updateButtonsNullEntry(List<? extends IImageEntry<?>> entries) {
950 boolean hasMultipleImages = entries != null && entries.size() > 1;
951 // if this method is called to reinitialize dialog content with a blank image,
952 // do not actually show the dialog again with a blank image if currently hidden (fix #10672)
953 this.updateTitle();
954 imgDisplay.setImage(null);
955 imgDisplay.setOsdText("");
956 setNextEnabled(false);
957 setPreviousEnabled(false);
958 btnDelete.setEnabled(hasMultipleImages);
959 btnDeleteFromDisk.setEnabled(hasMultipleImages);
960 btnCopyPath.setEnabled(false);
961 btnOpenExternal.setEnabled(false);
962 if (hasMultipleImages) {
963 imgDisplay.setEmptyText(tr("Multiple images selected"));
964 btnFirst.setEnabled(!isFirstImageSelected(entries));
965 btnLast.setEnabled(!isLastImageSelected(entries));
966 }
967 imgDisplay.setImage(null);
968 imgDisplay.setOsdText("");
969 }
970
971 /**
972 * Update the image viewer buttons for the new entry
973 * @param entry The new entry
974 * @param imageChanged {@code true} if it is not the same image as the previous image.
975 */
976 private void updateButtonsNonNullEntry(IImageEntry<?> entry, boolean imageChanged) {
977 if (imageChanged) {
978 cancelLoadingImage();
979 // don't show unwanted image
980 imgDisplay.setImage(null);
981 // Set only if the image is new to preserve zoom and position if the same image is redisplayed
982 // (e.g. to update the OSD).
983 imgLoadingFuture = imgDisplay.setImage(entry);
984 }
985
986 // Update buttons after setting the new entry
987 setNextEnabled(entry.getNextImage() != null);
988 setPreviousEnabled(entry.getPreviousImage() != null);
989 btnDelete.setEnabled(entry.isRemoveSupported());
990 btnDeleteFromDisk.setEnabled(entry.isDeleteSupported() && entry.isRemoveSupported());
991 btnCopyPath.setEnabled(true);
992 btnOpenExternal.setEnabled(true);
993
994 this.updateTitle();
995 StringBuilder osd = new StringBuilder(entry.getDisplayName());
996 if (entry.getElevation() != null) {
997 osd.append(tr("\nAltitude: {0} m", Math.round(entry.getElevation())));
998 }
999 if (entry.getSpeed() != null) {
1000 osd.append(tr("\nSpeed: {0} km/h", Math.round(entry.getSpeed())));
1001 }
1002 if (entry.getExifImgDir() != null) {
1003 osd.append(tr("\nDirection {0}\u00b0", Math.round(entry.getExifImgDir())));
1004 }
1005
1006 DateTimeFormatter dtf = DateUtils.getDateTimeFormatter(FormatStyle.SHORT, FormatStyle.MEDIUM)
1007 // Set timezone to UTC since UTC is assumed when parsing the EXIF timestamp,
1008 // see see org.openstreetmap.josm.tools.ExifReader.readTime(com.drew.metadata.Metadata)
1009 .withZone(ZoneOffset.UTC);
1010
1011 if (entry.hasExifTime()) {
1012 osd.append(tr("\nEXIF time: {0}", dtf.format(entry.getExifInstant())));
1013 }
1014 if (entry.hasGpsTime()) {
1015 osd.append(tr("\nGPS time: {0}", dtf.format(entry.getGpsInstant())));
1016 }
1017 Optional.ofNullable(entry.getIptcCaption()).map(s -> tr("\nCaption: {0}", s)).ifPresent(osd::append);
1018 Optional.ofNullable(entry.getIptcHeadline()).map(s -> tr("\nHeadline: {0}", s)).ifPresent(osd::append);
1019 Optional.ofNullable(entry.getIptcKeywords()).map(s -> tr("\nKeywords: {0}", s)).ifPresent(osd::append);
1020 Optional.ofNullable(entry.getIptcObjectName()).map(s -> tr("\nObject name: {0}", s)).ifPresent(osd::append);
1021
1022 imgDisplay.setOsdText(osd.toString());
1023 }
1024
1025 private void updateTitle() {
1026 final IImageEntry<?> entry;
1027 synchronized (this) {
1028 entry = this.currentEntry;
1029 }
1030 String baseTitle = Optional.ofNullable(this.layers.getSelectedComponent())
1031 .filter(MoveImgDisplayPanel.class::isInstance).map(MoveImgDisplayPanel.class::cast)
1032 .map(m -> m.layer).map(Layer::getLabel).orElse(tr("Geotagged Images"));
1033 if (entry == null) {
1034 this.setTitle(baseTitle);
1035 } else {
1036 this.setTitle(baseTitle + (!entry.getDisplayName().isEmpty() ? " - " + entry.getDisplayName() : ""));
1037 }
1038 }
1039
1040 private static boolean isLastImageSelected(List<? extends IImageEntry<?>> data) {
1041 return data.stream().anyMatch(image -> data.contains(image.getLastImage()));
1042 }
1043
1044 private static boolean isFirstImageSelected(List<? extends IImageEntry<?>> data) {
1045 return data.stream().anyMatch(image -> data.contains(image.getFirstImage()));
1046 }
1047
1048 /**
1049 * When an image is closed, really close it and do not pop
1050 * up the side dialog.
1051 */
1052 @Override
1053 protected boolean dockWhenClosingDetachedDlg() {
1054 if (collapseButtonClicked) {
1055 collapseButtonClicked = false;
1056 return super.dockWhenClosingDetachedDlg();
1057 }
1058 return false;
1059 }
1060
1061 @Override
1062 protected void stateChanged() {
1063 super.stateChanged();
1064 if (btnCollapse != null) {
1065 btnCollapse.setVisible(!isDocked);
1066 }
1067 }
1068
1069 /**
1070 * Returns whether an image is currently displayed
1071 * @return If image is currently displayed
1072 */
1073 public boolean hasImage() {
1074 return currentEntry != null;
1075 }
1076
1077 /**
1078 * Returns the currently displayed image.
1079 * @return Currently displayed image or {@code null}
1080 * @since 18246 (signature)
1081 */
1082 public static IImageEntry<?> getCurrentImage() {
1083 return getInstance().currentEntry;
1084 }
1085
1086 /**
1087 * Returns the rotation of the currently displayed image.
1088 * @param entry The entry to get the rotation for. May be {@code null}.
1089 * @return the rotation of the currently displayed image, or {@code null}
1090 * @since 18263
1091 */
1092 public Vector3D getRotation(IImageEntry<?> entry) {
1093 return imgDisplay.getRotation(entry);
1094 }
1095
1096 /**
1097 * Returns whether the center view is currently active.
1098 * @return {@code true} if the center view is active, {@code false} otherwise
1099 * @since 9416
1100 */
1101 public static boolean isCenterView() {
1102 return getInstance().centerView;
1103 }
1104
1105 @Override
1106 public void layerAdded(LayerAddEvent e) {
1107 registerOnLayer(e.getAddedLayer());
1108 showLayer(e.getAddedLayer());
1109 }
1110
1111 @Override
1112 public void layerRemoving(LayerRemoveEvent e) {
1113 if (e.getRemovedLayer() instanceof IGeoImageLayer && ((IGeoImageLayer) e.getRemovedLayer()).containsImage(this.currentEntry)) {
1114 displayImages(null);
1115 }
1116 this.updateLayers(true);
1117 if (!layers.isVisible()) {
1118 hideNotify();
1119 destroy();
1120 }
1121 }
1122
1123 @Override
1124 public void layerOrderChanged(LayerOrderChangeEvent e) {
1125 this.updateLayers(true);
1126 }
1127
1128 @Override
1129 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
1130 if (!MainApplication.worker.isShutdown()) {
1131 showLayer(e.getSource().getActiveLayer());
1132 }
1133 }
1134
1135 /**
1136 * Reload the image. Call this if you load a low-resolution image first, and then get a high-resolution image, or
1137 * if you know that the image has changed on disk.
1138 * @since 18591
1139 */
1140 public void refresh() {
1141 if (SwingUtilities.isEventDispatchThread()) {
1142 this.updateButtonsNonNullEntry(currentEntry, true);
1143 } else {
1144 GuiHelper.runInEDT(this::refresh);
1145 }
1146 }
1147
1148 private void registerOnLayer(Layer layer) {
1149 if (layer instanceof IGeoImageLayer) {
1150 layer.addPropertyChangeListener(l -> {
1151 final List<?> currentTabLayers = this.getImageTabs().map(m -> m.layer).collect(Collectors.toList());
1152 if (Layer.NAME_PROP.equals(l.getPropertyName()) && currentTabLayers.contains(layer)) {
1153 this.updateLayers(true);
1154 if (((IGeoImageLayer) layer).containsImage(this.currentEntry)) {
1155 this.updateTitle();
1156 }
1157 } // Use Layer.VISIBLE_PROP here if we decide to do something when layer visibility changes
1158 });
1159 }
1160 }
1161
1162 private void showLayer(Layer newLayer) {
1163 if (this.currentEntry == null && newLayer instanceof GeoImageLayer) {
1164 ImageData imageData = ((GeoImageLayer) newLayer).getImageData();
1165 imageData.setSelectedImage(imageData.getFirstImage());
1166 }
1167 if (newLayer instanceof IGeoImageLayer) {
1168 this.updateLayers(true);
1169 }
1170 }
1171
1172 private void cancelLoadingImage() {
1173 if (imgLoadingFuture != null) {
1174 imgLoadingFuture.cancel(false);
1175 imgLoadingFuture = null;
1176 }
1177 }
1178}
Note: See TracBrowser for help on using the repository browser.